diff --git a/src/mbgl/text/glyph.hpp b/src/mbgl/text/glyph.hpp index ba9c521f77b..09b90009e83 100644 --- a/src/mbgl/text/glyph.hpp +++ b/src/mbgl/text/glyph.hpp @@ -25,19 +25,16 @@ using GlyphIDs = std::set; GlyphRange getGlyphRange(GlyphID glyph); struct GlyphMetrics { - uint32_t width = 0; - uint32_t height = 0; + uint32_t width = 0U; + uint32_t height = 0U; int32_t left = 0; int32_t top = 0; - uint32_t advance = 0; + uint32_t advance = 0U; }; inline bool operator==(const GlyphMetrics& lhs, const GlyphMetrics& rhs) { - return lhs.width == rhs.width && - lhs.height == rhs.height && - lhs.left == rhs.left && - lhs.top == rhs.top && - lhs.advance == rhs.advance; + return lhs.width == rhs.width && lhs.height == rhs.height && lhs.left == rhs.left && lhs.top == rhs.top && + lhs.advance == rhs.advance; } class Glyph { @@ -55,7 +52,11 @@ class Glyph { GlyphMetrics metrics; }; -using Glyphs = std::map>>; +struct Glyphs { + std::map>> glyphs{}; + optional ascender{nullopt}; + optional descender{nullopt}; +}; using GlyphMap = std::map; class PositionedGlyph { @@ -82,7 +83,7 @@ class Shaping { Shaping() = default; explicit Shaping(float x, float y, WritingModeType writingMode_, std::size_t lineCount_) : top(y), bottom(y), left(x), right(x), writingMode(writingMode_), lineCount(lineCount_) {} - std::vector positionedGlyphs; + std::unordered_map> positionedGlyphs; float top = 0; float bottom = 0; float left = 0; @@ -92,6 +93,7 @@ class Shaping { explicit operator bool() const { return !positionedGlyphs.empty(); } // The y offset *should* be part of the font metadata. static constexpr int32_t yOffset = -17; + bool hasBaseline{false}; }; enum class WritingModeType : uint8_t { diff --git a/src/mbgl/text/glyph_atlas.cpp b/src/mbgl/text/glyph_atlas.cpp index da65aea8a9d..61dfb775a35 100644 --- a/src/mbgl/text/glyph_atlas.cpp +++ b/src/mbgl/text/glyph_atlas.cpp @@ -15,9 +15,10 @@ GlyphAtlas makeGlyphAtlas(const GlyphMap& glyphs) { for (const auto& glyphMapEntry : glyphs) { FontStackHash fontStack = glyphMapEntry.first; - GlyphPositionMap& positions = result.positions[fontStack]; - - for (const auto& entry : glyphMapEntry.second) { + GlyphPositionData& positions = result.positions[fontStack]; + positions.ascender = glyphMapEntry.second.ascender; + positions.descender = glyphMapEntry.second.descender; + for (const auto& entry : glyphMapEntry.second.glyphs) { if (entry.second && (*entry.second)->bitmap.valid()) { const Glyph& glyph = **entry.second; @@ -39,16 +40,12 @@ GlyphAtlas makeGlyphAtlas(const GlyphMap& glyphs) { }, glyph.bitmap.size); - positions.emplace(glyph.id, - GlyphPosition { - Rect { - static_cast(bin.x), - static_cast(bin.y), - static_cast(bin.w), - static_cast(bin.h) - }, - glyph.metrics - }); + positions.glyphPositionMap.emplace(glyph.id, + GlyphPosition{Rect{static_cast(bin.x), + static_cast(bin.y), + static_cast(bin.w), + static_cast(bin.h)}, + glyph.metrics}); } } } diff --git a/src/mbgl/text/glyph_atlas.hpp b/src/mbgl/text/glyph_atlas.hpp index 9dd063ef698..fa9140c3c2f 100644 --- a/src/mbgl/text/glyph_atlas.hpp +++ b/src/mbgl/text/glyph_atlas.hpp @@ -11,8 +11,13 @@ struct GlyphPosition { GlyphMetrics metrics; }; -using GlyphPositionMap = std::map; -using GlyphPositions = std::map; +struct GlyphPositionData { + std::map glyphPositionMap{}; + optional ascender{nullopt}; + optional descender{nullopt}; +}; + +using GlyphPositions = std::map; class GlyphAtlas { public: diff --git a/src/mbgl/text/glyph_manager.cpp b/src/mbgl/text/glyph_manager.cpp index 35ea1031d53..accae0cf9c8 100644 --- a/src/mbgl/text/glyph_manager.cpp +++ b/src/mbgl/text/glyph_manager.cpp @@ -89,9 +89,9 @@ void GlyphManager::processResponse(const Response& res, const FontStack& fontSta if (!res.noContent) { std::vector glyphs; - + int32_t ascender{0}, descender{0}; try { - glyphs = parseGlyphPBF(range, *res.data); + std::tie(glyphs, ascender, descender) = parseGlyphPBF(range, *res.data); } catch (...) { observer->onGlyphsError(fontStack, range, std::current_exception()); return; @@ -104,6 +104,10 @@ void GlyphManager::processResponse(const Response& res, const FontStack& fontSta entry.glyphs.emplace(id, makeMutable(std::move(glyph))); } } + if (ascender != 0 || descender != 0) { + entry.ascender = ascender; + entry.descender = descender; + } } request.parsed = true; @@ -134,18 +138,20 @@ void GlyphManager::notify(GlyphRequestor& requestor, const GlyphDependencies& gl Glyphs& glyphs = response[FontStackHasher()(fontStack)]; Entry& entry = entries[fontStack]; + glyphs.ascender = entry.ascender; + glyphs.descender = entry.descender; for (const auto& glyphID : glyphIDs) { auto it = entry.glyphs.find(glyphID); if (it != entry.glyphs.end()) { - glyphs.emplace(*it); + glyphs.glyphs.emplace(*it); } else { - glyphs.emplace(glyphID, std::experimental::nullopt); + glyphs.glyphs.emplace(glyphID, nullopt); } } } - requestor.onGlyphsAvailable(response); + requestor.onGlyphsAvailable(std::move(response)); } void GlyphManager::removeRequestor(GlyphRequestor& requestor) { diff --git a/src/mbgl/text/glyph_manager.hpp b/src/mbgl/text/glyph_manager.hpp index 8603a320d28..b4c8570926e 100644 --- a/src/mbgl/text/glyph_manager.hpp +++ b/src/mbgl/text/glyph_manager.hpp @@ -61,6 +61,8 @@ class GlyphManager { struct Entry { std::map ranges; std::map> glyphs; + optional ascender; + optional descender; }; std::unordered_map entries; diff --git a/src/mbgl/text/glyph_pbf.cpp b/src/mbgl/text/glyph_pbf.cpp index cfaf803f75e..dd9fe1638f2 100644 --- a/src/mbgl/text/glyph_pbf.cpp +++ b/src/mbgl/text/glyph_pbf.cpp @@ -4,17 +4,17 @@ namespace mbgl { -std::vector parseGlyphPBF(const GlyphRange& glyphRange, const std::string& data) { - std::vector result; - result.reserve(256); +std::tuple, int32_t, int32_t> parseGlyphPBF(const GlyphRange& glyphRange, const std::string& data) { + std::vector glyphs; + glyphs.reserve(256); + int32_t ascender{0}, descender{0}; + bool ascenderSet{false}, descenderSet{false}; protozero::pbf_reader glyphs_pbf(data); while (glyphs_pbf.next(1)) { - auto fontstack_pbf = glyphs_pbf.get_message(); - while (fontstack_pbf.next(3)) { + auto readGlyphMetrics = [glyphRange, &glyphs](protozero::pbf_reader& fontstack_pbf) { auto glyph_pbf = fontstack_pbf.get_message(); - Glyph glyph; protozero::data_view glyphData; @@ -23,36 +23,36 @@ std::vector parseGlyphPBF(const GlyphRange& glyphRange, const std::string while (glyph_pbf.next()) { switch (glyph_pbf.tag()) { - case 1: // id - glyph.id = glyph_pbf.get_uint32(); - hasID = true; - break; - case 2: // bitmap - glyphData = glyph_pbf.get_view(); - break; - case 3: // width - glyph.metrics.width = glyph_pbf.get_uint32(); - hasWidth = true; - break; - case 4: // height - glyph.metrics.height = glyph_pbf.get_uint32(); - hasHeight = true; - break; - case 5: // left - glyph.metrics.left = glyph_pbf.get_sint32(); - hasLeft = true; - break; - case 6: // top - glyph.metrics.top = glyph_pbf.get_sint32(); - hasTop = true; - break; - case 7: // advance - glyph.metrics.advance = glyph_pbf.get_uint32(); - hasAdvance = true; - break; - default: - glyph_pbf.skip(); - break; + case 1: // id + glyph.id = glyph_pbf.get_uint32(); + hasID = true; + break; + case 2: // bitmap + glyphData = glyph_pbf.get_view(); + break; + case 3: // width + glyph.metrics.width = glyph_pbf.get_uint32(); + hasWidth = true; + break; + case 4: // height + glyph.metrics.height = glyph_pbf.get_uint32(); + hasHeight = true; + break; + case 5: // left + glyph.metrics.left = glyph_pbf.get_sint32(); + hasLeft = true; + break; + case 6: // top + glyph.metrics.top = glyph_pbf.get_sint32(); + hasTop = true; + break; + case 7: // advance + glyph.metrics.advance = glyph_pbf.get_uint32(); + hasAdvance = true; + break; + default: + glyph_pbf.skip(); + break; } } @@ -60,35 +60,69 @@ std::vector parseGlyphPBF(const GlyphRange& glyphRange, const std::string // needs to satisfy a few metrics conditions that ensure that the glyph isn't bogus. // All other glyphs are malformed. We're also discarding all glyphs that are outside // the expected glyph range. - if (!hasID || !hasWidth || !hasHeight || !hasLeft || !hasTop || !hasAdvance || - glyph.metrics.width >= 256 || glyph.metrics.height >= 256 || - glyph.metrics.left < -128 || glyph.metrics.left >= 128 || - glyph.metrics.top < -128 || glyph.metrics.top >= 128 || - glyph.metrics.advance >= 256 || + if (!hasID || !hasWidth || !hasHeight || !hasLeft || !hasTop || !hasAdvance || glyph.metrics.width >= 256 || + glyph.metrics.height >= 256 || glyph.metrics.left < -128 || glyph.metrics.left >= 128 || + glyph.metrics.top < -128 || glyph.metrics.top >= 128 || glyph.metrics.advance >= 256 || glyph.id < glyphRange.first || glyph.id > glyphRange.second) { - continue; + return; } // If the area of width/height is non-zero, we need to adjust the expected size // with the implicit border size, otherwise we expect there to be no bitmap at all. if (glyph.metrics.width && glyph.metrics.height) { - const Size size { - glyph.metrics.width + 2 * Glyph::borderSize, - glyph.metrics.height + 2 * Glyph::borderSize - }; + const Size size{glyph.metrics.width + 2 * Glyph::borderSize, + glyph.metrics.height + 2 * Glyph::borderSize}; if (size.area() != glyphData.size()) { - continue; + return; } glyph.bitmap = AlphaImage(size, reinterpret_cast(glyphData.data()), glyphData.size()); } - result.push_back(std::move(glyph)); + glyphs.push_back(std::move(glyph)); + }; + + auto fontstack_pbf = glyphs_pbf.get_message(); + while (fontstack_pbf.next()) { + switch (fontstack_pbf.tag()) { + case 3: { + readGlyphMetrics(fontstack_pbf); + break; + } + case 4: { + // ascender value for one fontstack shall keep the same, if different values appear, set ascender to + // be 0/invalid. + const auto value = fontstack_pbf.get_sint32(); + if (!ascenderSet) { + ascender = value; + ascenderSet = true; + } else if (ascender != value) { + ascender = 0; + } + break; + } + case 5: { + // descender value for one fontstack shall keep the same, if different values appear, set descender + // to be 0/invalid. + const auto value = fontstack_pbf.get_sint32(); + if (!descenderSet) { + descender = value; + descenderSet = true; + } else if (descender != value) { + descender = 0; + } + break; + } + default: { + fontstack_pbf.skip(); + break; + } + } } } - return result; + return std::make_tuple(std::move(glyphs), ascender, descender); } } // namespace mbgl diff --git a/src/mbgl/text/glyph_pbf.hpp b/src/mbgl/text/glyph_pbf.hpp index 28a28b41143..5e938a63016 100644 --- a/src/mbgl/text/glyph_pbf.hpp +++ b/src/mbgl/text/glyph_pbf.hpp @@ -4,10 +4,11 @@ #include #include +#include #include namespace mbgl { -std::vector parseGlyphPBF(const GlyphRange&, const std::string& data); +std::tuple, int32_t, int32_t> parseGlyphPBF(const GlyphRange&, const std::string& data); } // namespace mbgl diff --git a/src/mbgl/text/quads.cpp b/src/mbgl/text/quads.cpp index 281c5d99de8..b2406fc4d33 100644 --- a/src/mbgl/text/quads.cpp +++ b/src/mbgl/text/quads.cpp @@ -68,89 +68,96 @@ SymbolQuads getGlyphQuads(const Shaping& shapedText, SymbolQuads quads; - for (const PositionedGlyph &positionedGlyph: shapedText.positionedGlyphs) { - auto fontPositions = positions.find(positionedGlyph.font); - if (fontPositions == positions.end()) - continue; - - auto positionsIt = fontPositions->second.find(positionedGlyph.glyph); - if (positionsIt == fontPositions->second.end()) - continue; - - const GlyphPosition& glyph = positionsIt->second; - const Rect& rect = glyph.rect; - - // The rects have an addditional buffer that is not included in their size; - const float glyphPadding = 1.0f; - const float rectBuffer = 3.0f + glyphPadding; - - const float halfAdvance = glyph.metrics.advance * positionedGlyph.scale / 2.0; - - const Point glyphOffset = alongLine ? - Point{ positionedGlyph.x + halfAdvance, positionedGlyph.y } : - Point{ 0.0f, 0.0f }; - - Point builtInOffset = alongLine ? - Point{ 0.0f, 0.0f } : - Point{ positionedGlyph.x + halfAdvance + textOffset[0], positionedGlyph.y + textOffset[1] }; - - Point verticalizedLabelOffset = { 0.0f, 0.0f }; - const bool rotateVerticalGlyph = (alongLine || allowVerticalPlacement) && positionedGlyph.vertical; - if (rotateVerticalGlyph) { - // Vertical POI labels, that are rotated 90deg CW and whose glyphs must preserve upright orientation - // need to be rotated 90deg CCW. After quad is rotated, it is translated to the original built-in offset. - verticalizedLabelOffset = builtInOffset; - builtInOffset = { 0.0f, 0.0f }; + if (shapedText.lineCount == 0) return quads; + const float lineHeight = (std::fabs(shapedText.bottom) + std::fabs(shapedText.top)) / shapedText.lineCount; + for (const auto& positionedGlyphs : shapedText.positionedGlyphs) { + const float currentHeight = lineHeight * positionedGlyphs.first; + for (const PositionedGlyph& positionedGlyph : positionedGlyphs.second) { + auto fontPositions = positions.find(positionedGlyph.font); + if (fontPositions == positions.end()) continue; + + auto positionsIt = fontPositions->second.glyphPositionMap.find(positionedGlyph.glyph); + if (positionsIt == fontPositions->second.glyphPositionMap.end()) { + continue; + } + + const GlyphPosition& glyph = positionsIt->second; + const Rect& rect = glyph.rect; + + // The rects have an additional buffer that is not included in their size; + const float glyphPadding = 1.0f; + const float rectBuffer = 3.0f + glyphPadding; + + const float halfAdvance = glyph.metrics.advance * positionedGlyph.scale / 2.0; + + const Point glyphOffset = + alongLine ? Point{positionedGlyph.x + halfAdvance, positionedGlyph.y} : Point{0.0f, 0.0f}; + + Point builtInOffset = alongLine ? Point{0.0f, 0.0f} + : Point{positionedGlyph.x + halfAdvance + textOffset[0], + positionedGlyph.y + textOffset[1]}; + + Point verticalizedLabelOffset = {0.0f, 0.0f}; + const bool rotateVerticalGlyph = (alongLine || allowVerticalPlacement) && positionedGlyph.vertical; + if (rotateVerticalGlyph) { + // Vertical POI labels, that are rotated 90deg CW and whose glyphs must preserve upright orientation + // need to be rotated 90deg CCW. After quad is rotated, it is translated to the original built-in + // offset. + verticalizedLabelOffset = builtInOffset; + builtInOffset = {0.0f, 0.0f}; + } + + const float x1 = (glyph.metrics.left - rectBuffer) * positionedGlyph.scale - halfAdvance + builtInOffset.x; + const float y1 = (-glyph.metrics.top - rectBuffer) * positionedGlyph.scale + builtInOffset.y; + const float x2 = x1 + rect.w * positionedGlyph.scale; + const float y2 = y1 + rect.h * positionedGlyph.scale; + + Point tl{x1, y1}; + Point tr{x2, y1}; + Point bl{x1, y2}; + Point br{x2, y2}; + + if (rotateVerticalGlyph) { + // Vertical-supporting glyphs are laid out in 24x24 point boxes (1 square em) + // In horizontal orientation, the y values for glyphs are below the midline. + // If the glyph's baseline is applicable, we take the relative y offset of the + // glyph, which needs to erase out current line height that added to the glyphs. + // Otherwise, we use a "yOffset" of -17 to pull them up to the middle. + // By rotating counter-clockwise around the point at the center of the left + // edge of a 24x24 layout box centered below the midline, we align the center + // of the glyphs with the horizontal midline, so the yOffset is no longer + // necessary, but we also pull the glyph to the left along the x axis. + // The y coordinate includes baseline yOffset, therefore, needs to be accounted + // for when glyph is rotated and translated. + const float yShift = (shapedText.hasBaseline ? (positionedGlyph.y - currentHeight) : Shaping::yOffset); + const Point center{-halfAdvance, halfAdvance - yShift}; + const float verticalRotation = -M_PI_2; + + // xHalfWidhtOffsetcorrection is a difference between full-width and half-width + // advance, should be 0 for full-width glyphs and will pull up half-width glyphs. + const float xHalfWidhtOffsetcorrection = util::ONE_EM / 2 - halfAdvance; + const Point xOffsetCorrection{5.0f - yShift - xHalfWidhtOffsetcorrection, 0.0f}; + + tl = util::rotate(tl - center, verticalRotation) + center + xOffsetCorrection + verticalizedLabelOffset; + tr = util::rotate(tr - center, verticalRotation) + center + xOffsetCorrection + verticalizedLabelOffset; + bl = util::rotate(bl - center, verticalRotation) + center + xOffsetCorrection + verticalizedLabelOffset; + br = util::rotate(br - center, verticalRotation) + center + xOffsetCorrection + verticalizedLabelOffset; + } + + if (textRotate) { + // Compute the transformation matrix. + float angle_sin = std::sin(textRotate); + float angle_cos = std::cos(textRotate); + std::array matrix = {{angle_cos, -angle_sin, angle_sin, angle_cos}}; + + tl = util::matrixMultiply(matrix, tl); + tr = util::matrixMultiply(matrix, tr); + bl = util::matrixMultiply(matrix, bl); + br = util::matrixMultiply(matrix, br); + } + + quads.emplace_back(tl, tr, bl, br, rect, shapedText.writingMode, glyphOffset, positionedGlyph.sectionIndex); } - - const float x1 = (glyph.metrics.left - rectBuffer) * positionedGlyph.scale - halfAdvance + builtInOffset.x; - const float y1 = (-glyph.metrics.top - rectBuffer) * positionedGlyph.scale + builtInOffset.y; - const float x2 = x1 + rect.w * positionedGlyph.scale; - const float y2 = y1 + rect.h * positionedGlyph.scale; - - Point tl{x1, y1}; - Point tr{x2, y1}; - Point bl{x1, y2}; - Point br{x2, y2}; - - if (rotateVerticalGlyph) { - // Vertical-supporting glyphs are laid out in 24x24 point boxes (1 square em) - // In horizontal orientation, the y values for glyphs are below the midline - // and we use a "yOffset" of -17 to pull them up to the middle. - // By rotating counter-clockwise around the point at the center of the left - // edge of a 24x24 layout box centered below the midline, we align the center - // of the glyphs with the horizontal midline, so the yOffset is no longer - // necessary, but we also pull the glyph to the left along the x axis. - // The y coordinate includes baseline yOffset, therefore, needs to be accounted - // for when glyph is rotated and translated. - - const Point center{ -halfAdvance, halfAdvance - Shaping::yOffset }; - const float verticalRotation = -M_PI_2; - - // xHalfWidhtOffsetcorrection is a difference between full-width and half-width - // advance, should be 0 for full-width glyphs and will pull up half-width glyphs. - const float xHalfWidhtOffsetcorrection = util::ONE_EM / 2 - halfAdvance; - const Point xOffsetCorrection{ 5.0f - Shaping::yOffset - xHalfWidhtOffsetcorrection, 0.0f }; - - tl = util::rotate(tl - center, verticalRotation) + center + xOffsetCorrection + verticalizedLabelOffset; - tr = util::rotate(tr - center, verticalRotation) + center + xOffsetCorrection + verticalizedLabelOffset; - bl = util::rotate(bl - center, verticalRotation) + center + xOffsetCorrection + verticalizedLabelOffset; - br = util::rotate(br - center, verticalRotation) + center + xOffsetCorrection + verticalizedLabelOffset; - } - - if (textRotate) { - // Compute the transformation matrix. - float angle_sin = std::sin(textRotate); - float angle_cos = std::cos(textRotate); - std::array matrix = {{angle_cos, -angle_sin, angle_sin, angle_cos}}; - - tl = util::matrixMultiply(matrix, tl); - tr = util::matrixMultiply(matrix, tr); - bl = util::matrixMultiply(matrix, bl); - br = util::matrixMultiply(matrix, br); - } - - quads.emplace_back(tl, tr, bl, br, rect, shapedText.writingMode, glyphOffset, positionedGlyph.sectionIndex); } return quads; diff --git a/src/mbgl/text/shaping.cpp b/src/mbgl/text/shaping.cpp index d6d9a3d34e0..02d53598b3b 100644 --- a/src/mbgl/text/shaping.cpp +++ b/src/mbgl/text/shaping.cpp @@ -122,10 +122,12 @@ void align(Shaping& shaping, const std::size_t lineCount) { const float shiftX = (justify - horizontalAlign) * maxLineLength; const float shiftY = (-verticalAlign * lineCount + 0.5) * lineHeight; - - for (auto& glyph : shaping.positionedGlyphs) { - glyph.x += shiftX; - glyph.y += shiftY; + + for (auto& glyphs : shaping.positionedGlyphs) { + for (auto& glyph : glyphs.second) { + glyph.x += shiftX; + glyph.y += shiftY; + } } } @@ -134,23 +136,25 @@ void justifyLine(std::vector& positionedGlyphs, const GlyphMap& glyphMap, std::size_t start, std::size_t end, - float justify) { - if (!justify) { + float justify, + float baselineOffset) { + if (!justify && !baselineOffset) { return; } - + PositionedGlyph& glyph = positionedGlyphs[end]; auto glyphs = glyphMap.find(glyph.font); if (glyphs == glyphMap.end()) { return; } - auto it = glyphs->second.find(glyph.glyph); - if (it != glyphs->second.end() && it->second) { + auto it = glyphs->second.glyphs.find(glyph.glyph); + if (it != glyphs->second.glyphs.end() && it->second) { const float lastAdvance = (*it->second)->metrics.advance * glyph.scale; const float lineIndent = float(glyph.x + lastAdvance) * justify; - + for (std::size_t j = start; j <= end; j++) { positionedGlyphs[j].x -= lineIndent; + positionedGlyphs[j].y += baselineOffset; } } } @@ -168,11 +172,11 @@ float determineAverageLineWidth(const TaggedString& logicalInput, if (glyphs == glyphMap.end()) { continue; } - auto it = glyphs->second.find(codePoint); - if (it == glyphs->second.end() || !it->second) { + auto it = glyphs->second.glyphs.find(codePoint); + if (it == glyphs->second.glyphs.end() || !it->second) { continue; } - + totalWidth += (*it->second)->metrics.advance * section.scale + spacing; } @@ -294,11 +298,11 @@ std::set determineLineBreaks(const TaggedString& logicalInput, if (glyphs == glyphMap.end()) { continue; } - auto it = glyphs->second.find(codePoint); - if (it != glyphs->second.end() && it->second && !util::i18n::isWhitespace(codePoint)) { + auto it = glyphs->second.glyphs.find(codePoint); + if (it != glyphs->second.glyphs.end() && it->second && !util::i18n::isWhitespace(codePoint)) { currentX += (*it->second)->metrics.advance * section.scale + spacing; } - + // Ideographic characters, spaces, and word-breaking punctuation that often appear without // surrounding spaces. if (i < logicalInput.length() - 1) { @@ -325,16 +329,29 @@ void shapeLines(Shaping& shaping, const WritingModeType writingMode, const GlyphMap& glyphMap, bool allowVerticalPlacement) { - float x = 0; - float y = Shaping::yOffset; - - float maxLineLength = 0; - + float x = 0.0f; + float y = 0.0f; + float maxLineLength = 0.0f; + bool hasBaseline{false}; + + for (const auto& line : lines) { + const auto& sections = line.getSections(); + for (const auto& section : sections) { + auto glyphs = glyphMap.find(section.fontStackHash); + if (glyphs == glyphMap.end()) { + continue; + } + hasBaseline = (glyphs->second.ascender.has_value() && glyphs->second.descender.has_value()); + if (!hasBaseline) break; + } + if (!hasBaseline) break; + } const float justify = textJustify == style::TextJustifyType::Right ? 1 : textJustify == style::TextJustifyType::Left ? 0 : 0.5; - + + uint32_t lineIndex{0}; for (TaggedString& line : lines) { // Collapse whitespace so it doesn't throw off justification line.trim(); @@ -343,10 +360,11 @@ void shapeLines(Shaping& shaping, if (line.empty()) { y += lineHeight; // Still need a line feed after empty line + ++lineIndex; continue; } - - std::size_t lineStartIndex = shaping.positionedGlyphs.size(); + + float biggestHeight{0.0f}, baselineOffset{0.0f}; for (std::size_t i = 0; i < line.length(); i++) { const std::size_t sectionIndex = line.getSectionIndex(i); const SectionOptions& section = line.sectionAt(sectionIndex); @@ -355,56 +373,89 @@ void shapeLines(Shaping& shaping, if (glyphs == glyphMap.end()) { continue; } - auto it = glyphs->second.find(codePoint); - if (it == glyphs->second.end() || !it->second) { + auto it = glyphs->second.glyphs.find(codePoint); + if (it == glyphs->second.glyphs.end() || !it->second) { continue; } - - // We don't know the baseline, but since we're laying out - // at 24 points, we can calculate how much it will move when - // we scale up or down. - const double baselineOffset = (lineMaxScale - section.scale) * util::ONE_EM; - + const Glyph& glyph = **it->second; + float ascender{0.0f}, descender{0.0f}, glyphOffset{0.0f}; + // In order to make different fonts aligned, they must share a general baseline that aligns with every + // font's real baseline. Glyph's offset is counted from the top left corner, where is the ascender line + // starts. First of all, every glyph's baseline lies on the middle line of each shaping line. Since ascender + // is above the baseline, the glyphOffset is the negative shift. Then, in order to make glyphs fit in the + // shaping box, for each line, we shift the glyph with biggest height(with scale) to make its middle line + // lie on the middle line of the line, which will lead to a baseline shift. Then adjust the whole line with + // the baseline offset we calculated from the shift. + if (hasBaseline) { + assert(glyphs->second.ascender && glyphs->second.descender); + ascender = std::abs(glyphs->second.ascender.value()); + descender = std::abs(glyphs->second.descender.value()); + auto value = (ascender + descender) * section.scale; + if (biggestHeight < value) { + biggestHeight = value; + baselineOffset = (ascender - descender) / 2 * section.scale; + } + glyphOffset = -ascender * section.scale; + } else { + // If font's baseline is not applicable, fall back to use a default baseline offset, see + // Shaping::yOffset. Since we're laying out at 24 points, we need also calculate how much it will move + // when we scale up or down. + glyphOffset = Shaping::yOffset + (lineMaxScale - section.scale) * util::ONE_EM; + } + if (writingMode == WritingModeType::Horizontal || // Don't verticalize glyphs that have no upright orientation if vertical placement is disabled. (!allowVerticalPlacement && !util::i18n::hasUprightVerticalOrientation(codePoint)) || // If vertical placement is ebabled, don't verticalize glyphs that // are from complex text layout script, or whitespaces. - (allowVerticalPlacement && (util::i18n::isWhitespace(codePoint) || util::i18n::isCharInComplexShapingScript(codePoint)))) { - shaping.positionedGlyphs.emplace_back(codePoint, x, y + baselineOffset, false, section.fontStackHash, section.scale, sectionIndex); + (allowVerticalPlacement && + (util::i18n::isWhitespace(codePoint) || util::i18n::isCharInComplexShapingScript(codePoint)))) { + shaping.positionedGlyphs[lineIndex].emplace_back( + codePoint, x, y + glyphOffset, false, section.fontStackHash, section.scale, sectionIndex); x += glyph.metrics.advance * section.scale + spacing; } else { - shaping.positionedGlyphs.emplace_back(codePoint, x, y + baselineOffset, true, section.fontStackHash, section.scale, sectionIndex); + shaping.positionedGlyphs[lineIndex].emplace_back( + codePoint, x, y + glyphOffset, true, section.fontStackHash, section.scale, sectionIndex); x += util::ONE_EM * section.scale + spacing; } } - + // Only justify if we placed at least one glyph - if (shaping.positionedGlyphs.size() != lineStartIndex) { + if (!shaping.positionedGlyphs[lineIndex].empty()) { float lineLength = x - spacing; // Don't count trailing spacing maxLineLength = util::max(lineLength, maxLineLength); - - justifyLine(shaping.positionedGlyphs, glyphMap, lineStartIndex, - shaping.positionedGlyphs.size() - 1, justify); + + justifyLine(shaping.positionedGlyphs[lineIndex], + glyphMap, + 0, + shaping.positionedGlyphs[lineIndex].size() - 1, + justify, + baselineOffset); } - + x = 0; y += lineHeight * lineMaxScale; + ++lineIndex; } auto anchorAlign = AnchorAlignment::getAnchorAlignment(textAnchor); - align(shaping, justify, anchorAlign.horizontalAlign, anchorAlign.verticalAlign, maxLineLength, - lineHeight, lines.size()); - const float height = y - Shaping::yOffset; - + align(shaping, + justify, + anchorAlign.horizontalAlign, + anchorAlign.verticalAlign, + maxLineLength, + lineHeight, + lines.size()); + const float height = y; // Calculate the bounding box shaping.top += -anchorAlign.verticalAlign * height; shaping.bottom = shaping.top + height; shaping.left += -anchorAlign.horizontalAlign * maxLineLength; shaping.right = shaping.left + maxLineLength; + shaping.hasBaseline = hasBaseline; } const Shaping getShaping(const TaggedString& formattedString, diff --git a/src/mbgl/text/tagged_string.hpp b/src/mbgl/text/tagged_string.hpp index 698e539a455..966ef0bc610 100644 --- a/src/mbgl/text/tagged_string.hpp +++ b/src/mbgl/text/tagged_string.hpp @@ -38,12 +38,12 @@ struct SectionOptions { struct TaggedString { TaggedString() = default; - TaggedString(std::u16string text_, SectionOptions options) - : styledText(std::move(text_), - std::vector(text_.size(), 0)) { + TaggedString(std::u16string text_, SectionOptions options) { + styledText.second = std::vector(text_.size(), 0); + styledText.first = std::move(text_); sections.push_back(std::move(options)); } - + TaggedString(StyledText styledText_, std::vector sections_) : styledText(std::move(styledText_)) , sections(std::move(sections_)) { diff --git a/src/mbgl/tile/geometry_tile_worker.cpp b/src/mbgl/tile/geometry_tile_worker.cpp index 7815e62b75e..5bff71bdc6b 100644 --- a/src/mbgl/tile/geometry_tile_worker.cpp +++ b/src/mbgl/tile/geometry_tile_worker.cpp @@ -259,18 +259,21 @@ void GeometryTileWorker::onGlyphsAvailable(GlyphMap newGlyphMap) { Glyphs& newGlyphs = newFontGlyphs.second; Glyphs& glyphs = glyphMap[fontStack]; + glyphs.ascender = newGlyphs.ascender; + glyphs.descender = newGlyphs.descender; + for (auto& pendingGlyphDependency : pendingGlyphDependencies) { // Linear lookup here to handle reverse of FontStackHash -> FontStack, // since dependencies need the full font stack name to make a request // There should not be many fontstacks to look through if (FontStackHasher()(pendingGlyphDependency.first) == fontStack) { GlyphIDs& pendingGlyphIDs = pendingGlyphDependency.second; - for (auto& newGlyph : newGlyphs) { + for (auto& newGlyph : newGlyphs.glyphs) { const GlyphID& glyphID = newGlyph.first; optional>& glyph = newGlyph.second; if (pendingGlyphIDs.erase(glyphID)) { - glyphs.emplace(glyphID, std::move(glyph)); + glyphs.glyphs.emplace(glyphID, std::move(glyph)); } } } @@ -294,7 +297,8 @@ void GeometryTileWorker::requestNewGlyphs(const GlyphDependencies& glyphDependen for (auto& fontDependencies : glyphDependencies) { auto fontGlyphs = glyphMap.find(FontStackHasher()(fontDependencies.first)); for (auto glyphID : fontDependencies.second) { - if (fontGlyphs == glyphMap.end() || fontGlyphs->second.find(glyphID) == fontGlyphs->second.end()) { + if (fontGlyphs == glyphMap.end() || + fontGlyphs->second.glyphs.find(glyphID) == fontGlyphs->second.glyphs.end()) { pendingGlyphDependencies[fontDependencies.first].insert(glyphID); } } diff --git a/test/text/glyph_manager.test.cpp b/test/text/glyph_manager.test.cpp index 1e9aef38ede..90551a5f1fd 100644 --- a/test/text/glyph_manager.test.cpp +++ b/test/text/glyph_manager.test.cpp @@ -106,8 +106,8 @@ TEST(GlyphManager, LoadingSuccess) { ASSERT_EQ(range, GlyphRange(0, 255)); }; - test.requestor.glyphsAvailable = [&] (GlyphMap glyphs) { - const auto& testPositions = glyphs.at(FontStackHasher()({{"Test Stack"}})); + test.requestor.glyphsAvailable = [&](GlyphMap glyphs) { + const auto& testPositions = glyphs.at(FontStackHasher()({{"Test Stack"}})).glyphs; ASSERT_EQ(testPositions.size(), 3u); ASSERT_EQ(testPositions.count(u'a'), 1u); @@ -223,8 +223,8 @@ TEST(GlyphManager, LoadLocalCJKGlyph) { test.requestor.glyphsAvailable = [&] (GlyphMap glyphs) { EXPECT_EQ(glyphResponses, 0); // Local generation should prevent requesting any glyphs - - const auto& testPositions = glyphs.at(FontStackHasher()({{"Test Stack"}})); + + const auto& testPositions = glyphs.at(FontStackHasher()({{"Test Stack"}})).glyphs; ASSERT_EQ(testPositions.size(), 1u); ASSERT_EQ(testPositions.count(u'中'), 1u); @@ -264,9 +264,9 @@ TEST(GlyphManager, LoadLocalCJKGlyphAfterLoadingRangeFromURL) { return response; }; - - test.requestor.glyphsAvailable = [&] (GlyphMap glyphs) { - const auto& testPositions = glyphs.at(FontStackHasher()({{"Test Stack"}})); + + test.requestor.glyphsAvailable = [&](GlyphMap glyphs) { + const auto& testPositions = glyphs.at(FontStackHasher()({{"Test Stack"}})).glyphs; if (firstGlyphResponse == true) { firstGlyphResponse = false; @@ -325,8 +325,8 @@ TEST(GlyphManager, LoadingInvalid) { ASSERT_EQ(range, GlyphRange(0, 255)); }; - test.requestor.glyphsAvailable = [&] (GlyphMap glyphs) { - const auto& testPositions = glyphs.at(FontStackHasher()({{"Test Stack"}})); + test.requestor.glyphsAvailable = [&](GlyphMap glyphs) { + const auto& testPositions = glyphs.at(FontStackHasher()({{"Test Stack"}})).glyphs; ASSERT_EQ(testPositions.size(), 2u); ASSERT_FALSE(bool(testPositions.at(u'A'))); diff --git a/test/text/glyph_pbf.test.cpp b/test/text/glyph_pbf.test.cpp index c222ec1dd9a..462f073b22c 100644 --- a/test/text/glyph_pbf.test.cpp +++ b/test/text/glyph_pbf.test.cpp @@ -7,10 +7,12 @@ using namespace mbgl; TEST(GlyphPBF, Parsing) { // The fake glyphs contain a number of invalid glyphs, which should be skipped by the parser. - auto sdfs = parseGlyphPBF(GlyphRange { 0, 255 }, util::read_file("test/fixtures/resources/fake_glyphs-0-255.pbf")); - EXPECT_TRUE(sdfs.size() == 1); - - auto& sdf = sdfs[0]; + std::vector sdfs; + int32_t ascender, descender; + std::tie(sdfs, ascender, descender) = + parseGlyphPBF(GlyphRange{0, 255}, util::read_file("test/fixtures/resources/fake_glyphs-0-255.pbf")); + ASSERT_EQ(1, sdfs.size()); + const auto& sdf = sdfs[0]; EXPECT_EQ(69u, sdf.id); AlphaImage expected({7, 7}); expected.fill('x'); @@ -20,4 +22,6 @@ TEST(GlyphPBF, Parsing) { EXPECT_EQ(20, sdf.metrics.left); EXPECT_EQ(2, sdf.metrics.top); EXPECT_EQ(8u, sdf.metrics.advance); + EXPECT_EQ(0, ascender); + EXPECT_EQ(0, descender); } diff --git a/test/text/quads.test.cpp b/test/text/quads.test.cpp index 7aaeb4870df..7a1651c7f49 100644 --- a/test/text/quads.test.cpp +++ b/test/text/quads.test.cpp @@ -47,7 +47,7 @@ TEST(getIconQuads, style) { shapedText.bottom = 30.0f; shapedText.left = -60.0f; shapedText.right = 20.0f; - shapedText.positionedGlyphs.emplace_back(PositionedGlyph(32, 0.0f, 0.0f, false, 0, 1.0)); + shapedText.positionedGlyphs[0].emplace_back(PositionedGlyph(32, 0.0f, 0.0f, false, 0, 1.0)); // none { diff --git a/test/text/shaping.test.cpp b/test/text/shaping.test.cpp index b22cd7da369..b7f4589f67c 100644 --- a/test/text/shaping.test.cpp +++ b/test/text/shaping.test.cpp @@ -6,6 +6,8 @@ #include #include +#include + using namespace mbgl; using namespace util; @@ -22,17 +24,17 @@ TEST(Shaping, ZWSP) { auto immutableGlyph = Immutable(makeMutable(std::move(glyph))); const std::vector fontStack{{"font-stack"}}; const SectionOptions sectionOptions(1.0f, fontStack); - GlyphMap glyphs = { - { FontStackHasher()(fontStack), {{u'中', std::move(immutableGlyph)}} } - }; + Glyphs glyphData; + glyphData.glyphs.emplace(u'中', std::move(immutableGlyph)); + GlyphMap glyphs = {{FontStackHasher()(fontStack), std::move(glyphData)}}; - const auto testGetShaping = [&] (const TaggedString& string, unsigned maxWidthInChars) { + const auto testGetShaping = [&](const TaggedString& string, unsigned maxWidthInChars) { return getShaping(string, maxWidthInChars * ONE_EM, - ONE_EM, // lineHeight + ONE_EM, // lineHeight style::SymbolAnchorType::Center, style::TextJustifyType::Center, - 0, // spacing + 0, // spacing {{0.0f, 0.0f}}, // translate WritingModeType::Horizontal, bidi, @@ -48,11 +50,13 @@ TEST(Shaping, ZWSP) { TaggedString string(u"中中\u200b中中\u200b中中\u200b中中中中中中\u200b中中", sectionOptions); auto shaping = testGetShaping(string, 5); ASSERT_EQ(shaping.lineCount, 3); - ASSERT_EQ(shaping.top, -36); - ASSERT_EQ(shaping.bottom, 36); - ASSERT_EQ(shaping.left, -63); - ASSERT_EQ(shaping.right, 63); + ASSERT_FLOAT_EQ(-36.0f, shaping.top); + ASSERT_FLOAT_EQ(36.0f, shaping.bottom); + ASSERT_FLOAT_EQ(-63.0f, shaping.left); + ASSERT_FLOAT_EQ(63.0f, shaping.right); ASSERT_EQ(shaping.writingMode, WritingModeType::Horizontal); + ASSERT_EQ(shaping.lineCount, shaping.positionedGlyphs.size()); + ASSERT_FALSE(shaping.hasBaseline); } // 2 lines @@ -62,11 +66,13 @@ TEST(Shaping, ZWSP) { TaggedString string(u"中中\u200b中", sectionOptions); auto shaping = testGetShaping(string, 1); ASSERT_EQ(shaping.lineCount, 2); - ASSERT_EQ(shaping.top, -24); - ASSERT_EQ(shaping.bottom, 24); - ASSERT_EQ(shaping.left, -21); - ASSERT_EQ(shaping.right, 21); + ASSERT_FLOAT_EQ(-24.0f, shaping.top); + ASSERT_FLOAT_EQ(24.0f, shaping.bottom); + ASSERT_FLOAT_EQ(-21.0f, shaping.left); + ASSERT_FLOAT_EQ(21.0f, shaping.right); ASSERT_EQ(shaping.writingMode, WritingModeType::Horizontal); + ASSERT_EQ(shaping.lineCount, shaping.positionedGlyphs.size()); + ASSERT_FALSE(shaping.hasBaseline); } // 1 line @@ -75,11 +81,20 @@ TEST(Shaping, ZWSP) { TaggedString string(u"中中\u200b", sectionOptions); auto shaping = testGetShaping(string, 2); ASSERT_EQ(shaping.lineCount, 1); - ASSERT_EQ(shaping.top, -12); - ASSERT_EQ(shaping.bottom, 12); - ASSERT_EQ(shaping.left, -21); - ASSERT_EQ(shaping.right, 21); + ASSERT_FLOAT_EQ(-12.0f, shaping.top); + ASSERT_FLOAT_EQ(12.0f, shaping.bottom); + ASSERT_FLOAT_EQ(-21.0f, shaping.left); + ASSERT_FLOAT_EQ(21.0f, shaping.right); ASSERT_EQ(shaping.writingMode, WritingModeType::Horizontal); + ASSERT_EQ(shaping.lineCount, shaping.positionedGlyphs.size()); + ASSERT_EQ(1, shaping.positionedGlyphs.count(0)); + ASSERT_EQ(2, shaping.positionedGlyphs[0].size()); + const auto& glyphs = shaping.positionedGlyphs[0]; + ASSERT_FLOAT_EQ(-21.0f, glyphs[0].x); + ASSERT_FLOAT_EQ(-17.0f, glyphs[0].y); + ASSERT_FLOAT_EQ(0.0f, glyphs[1].x); + ASSERT_FLOAT_EQ(-17.0f, glyphs[1].y); + ASSERT_FALSE(shaping.hasBaseline); } // 5 'new' lines. @@ -87,10 +102,245 @@ TEST(Shaping, ZWSP) { TaggedString string(u"\u200b\u200b\u200b\u200b\u200b", sectionOptions); auto shaping = testGetShaping(string, 1); ASSERT_EQ(shaping.lineCount, 5); - ASSERT_EQ(shaping.top, -60); - ASSERT_EQ(shaping.bottom, 60); - ASSERT_EQ(shaping.left, 0); - ASSERT_EQ(shaping.right, 0); + ASSERT_FLOAT_EQ(-60.0f, shaping.top); + ASSERT_FLOAT_EQ(60.0f, shaping.bottom); + ASSERT_FLOAT_EQ(-0.0f, shaping.left); + ASSERT_FLOAT_EQ(0.0f, shaping.right); + ASSERT_EQ(shaping.writingMode, WritingModeType::Horizontal); + ASSERT_EQ(shaping.lineCount, shaping.positionedGlyphs.size()); + ASSERT_FALSE(shaping.hasBaseline); + } +} + +TEST(Shaping, MixedFontsBothWithBaselines) { + Glyph glyph1; + glyph1.id = u'阳'; + glyph1.metrics.width = 18; + glyph1.metrics.height = 19; + glyph1.metrics.left = 2; + glyph1.metrics.top = -8; + glyph1.metrics.advance = 21; + + Glyph glyph2; + glyph2.id = u'光'; + glyph2.metrics.width = 18; + glyph2.metrics.height = 18; + glyph2.metrics.left = 2; + glyph2.metrics.top = -8; + glyph2.metrics.advance = 21; + + BiDi bidi; + std::vector sectionOptions; + const std::vector fontStack1{{"font-stack1"}}; + sectionOptions.emplace_back(1.0f, fontStack1); + Glyphs glyphData1; + glyphData1.glyphs.emplace(u'阳', Immutable(makeMutable(std::move(glyph1)))); + glyphData1.ascender = 26; + glyphData1.descender = -6; + + const std::vector fontStack2{{"font-stack2"}}; + sectionOptions.emplace_back(1.0f, fontStack2); + Glyphs glyphData2; + glyphData2.glyphs.emplace(u'光', Immutable(makeMutable(std::move(glyph2)))); + glyphData2.ascender = 25; + glyphData2.descender = -5; + + GlyphMap glyphs; + glyphs.emplace(FontStackHasher()(fontStack1), std::move(glyphData1)); + glyphs.emplace(FontStackHasher()(fontStack2), std::move(glyphData2)); + + const auto testGetShaping = [&](const TaggedString& string, unsigned maxWidthInChars) { + return getShaping(string, + maxWidthInChars * ONE_EM, + ONE_EM, // lineHeight + style::SymbolAnchorType::Center, + style::TextJustifyType::Center, + 0, // spacing + {{0.0f, 0.0f}}, // translate + WritingModeType::Horizontal, + bidi, + glyphs, + /*allowVerticalPlacement*/ false); + }; + + { + std::u16string text{u"阳光\u200b"}; + StyledText styledText; + styledText.second = std::vector{0, 1, 0}; + styledText.first = std::move(text); + + TaggedString string{styledText, sectionOptions}; + + auto shaping = testGetShaping(string, 5); + ASSERT_EQ(shaping.lineCount, 1); + ASSERT_FLOAT_EQ(-12.0f, shaping.top); + ASSERT_FLOAT_EQ(12.0f, shaping.bottom); + ASSERT_FLOAT_EQ(-21.0f, shaping.left); + ASSERT_FLOAT_EQ(21.0f, shaping.right); + ASSERT_EQ(shaping.writingMode, WritingModeType::Horizontal); + ASSERT_EQ(shaping.lineCount, shaping.positionedGlyphs.size()); + ASSERT_EQ(1, shaping.positionedGlyphs.count(0)); + ASSERT_EQ(2, shaping.positionedGlyphs[0].size()); + const auto& glyphs = shaping.positionedGlyphs[0]; + ASSERT_FLOAT_EQ(-21.0f, glyphs[0].x); + ASSERT_FLOAT_EQ(-16.0f, glyphs[0].y); + ASSERT_FLOAT_EQ(0.0f, glyphs[1].x); + ASSERT_FLOAT_EQ(-15.0f, glyphs[1].y); + ASSERT_TRUE(shaping.hasBaseline); + } +} + +TEST(Shaping, MixedFontsOneWithBaselineOneWithout) { + Glyph glyph1; + glyph1.id = u'阳'; + glyph1.metrics.width = 18; + glyph1.metrics.height = 19; + glyph1.metrics.left = 2; + glyph1.metrics.top = -8; + glyph1.metrics.advance = 21; + + Glyph glyph2; + glyph2.id = u'光'; + glyph2.metrics.width = 18; + glyph2.metrics.height = 18; + glyph2.metrics.left = 2; + glyph2.metrics.top = -8; + glyph2.metrics.advance = 21; + + BiDi bidi; + std::vector sectionOptions; + const std::vector fontStack1{{"font-stack1"}}; + sectionOptions.emplace_back(1.0f, fontStack1); + Glyphs glyphData1; + glyphData1.glyphs.emplace(u'阳', Immutable(makeMutable(std::move(glyph1)))); + glyphData1.ascender = 26; + glyphData1.descender = -6; + + const std::vector fontStack2{{"font-stack2"}}; + sectionOptions.emplace_back(1.0f, fontStack2); + Glyphs glyphData2; + glyphData2.glyphs.emplace(u'光', Immutable(makeMutable(std::move(glyph2)))); + + GlyphMap glyphs; + glyphs.emplace(FontStackHasher()(fontStack1), std::move(glyphData1)); + glyphs.emplace(FontStackHasher()(fontStack2), std::move(glyphData2)); + + const auto testGetShaping = [&](const TaggedString& string, unsigned maxWidthInChars) { + return getShaping(string, + maxWidthInChars * ONE_EM, + ONE_EM, // lineHeight + style::SymbolAnchorType::Center, + style::TextJustifyType::Center, + 0, // spacing + {{0.0f, 0.0f}}, // translate + WritingModeType::Horizontal, + bidi, + glyphs, + /*allowVerticalPlacement*/ false); + }; + + { + std::u16string text{u"阳光\u200b"}; + StyledText styledText; + styledText.second = std::vector{0, 1, 0}; + styledText.first = std::move(text); + + TaggedString string{styledText, sectionOptions}; + + auto shaping = testGetShaping(string, 5); + ASSERT_EQ(shaping.lineCount, 1); + ASSERT_FLOAT_EQ(-12.0f, shaping.top); + ASSERT_FLOAT_EQ(12.0f, shaping.bottom); + ASSERT_FLOAT_EQ(-21.0f, shaping.left); + ASSERT_FLOAT_EQ(21.0f, shaping.right); + ASSERT_EQ(shaping.writingMode, WritingModeType::Horizontal); + ASSERT_EQ(shaping.lineCount, shaping.positionedGlyphs.size()); + ASSERT_EQ(1, shaping.positionedGlyphs.count(0)); + ASSERT_EQ(2, shaping.positionedGlyphs[0].size()); + const auto& glyphs = shaping.positionedGlyphs[0]; + ASSERT_FLOAT_EQ(-21.0f, glyphs[0].x); + ASSERT_FLOAT_EQ(-17.0f, glyphs[0].y); + ASSERT_FLOAT_EQ(0.0f, glyphs[1].x); + ASSERT_FLOAT_EQ(-17.0f, glyphs[1].y); + ASSERT_FALSE(shaping.hasBaseline); + } +} + +TEST(Shaping, MixedFontsWithBaselineWithFontScale) { + Glyph glyph1; + glyph1.id = u'阳'; + glyph1.metrics.width = 18; + glyph1.metrics.height = 19; + glyph1.metrics.left = 2; + glyph1.metrics.top = -8; + glyph1.metrics.advance = 21; + + Glyph glyph2; + glyph2.id = u'光'; + glyph2.metrics.width = 18; + glyph2.metrics.height = 18; + glyph2.metrics.left = 2; + glyph2.metrics.top = -8; + glyph2.metrics.advance = 21; + + BiDi bidi; + std::vector sectionOptions; + const std::vector fontStack1{{"font-stack1"}}; + sectionOptions.emplace_back(1.0f, fontStack1); + sectionOptions.back().scale = 1.5; + Glyphs glyphData1; + glyphData1.glyphs.emplace(u'阳', Immutable(makeMutable(std::move(glyph1)))); + glyphData1.ascender = 26; + glyphData1.descender = -6; + + const std::vector fontStack2{{"font-stack2"}}; + sectionOptions.emplace_back(1.0f, fontStack2); + Glyphs glyphData2; + glyphData2.glyphs.emplace(u'光', Immutable(makeMutable(std::move(glyph2)))); + glyphData2.ascender = 25; + glyphData2.descender = -5; + + GlyphMap glyphs; + glyphs.emplace(FontStackHasher()(fontStack1), std::move(glyphData1)); + glyphs.emplace(FontStackHasher()(fontStack2), std::move(glyphData2)); + + const auto testGetShaping = [&](const TaggedString& string, unsigned maxWidthInChars) { + return getShaping(string, + maxWidthInChars * ONE_EM, + ONE_EM, // lineHeight + style::SymbolAnchorType::Center, + style::TextJustifyType::Center, + 0, // spacing + {{0.0f, 0.0f}}, // translate + WritingModeType::Horizontal, + bidi, + glyphs, + /*allowVerticalPlacement*/ false); + }; + + { + std::u16string text{u"阳光\u200b"}; + StyledText styledText; + styledText.second = std::vector{0, 1, 0}; + styledText.first = std::move(text); + + TaggedString string{styledText, sectionOptions}; + + auto shaping = testGetShaping(string, 5); + ASSERT_EQ(shaping.lineCount, 1); + ASSERT_FLOAT_EQ(-18.0f, shaping.top); + ASSERT_FLOAT_EQ(18.0f, shaping.bottom); + ASSERT_FLOAT_EQ(-26.25f, shaping.left); + ASSERT_FLOAT_EQ(26.25f, shaping.right); ASSERT_EQ(shaping.writingMode, WritingModeType::Horizontal); + ASSERT_EQ(shaping.lineCount, shaping.positionedGlyphs.size()); + ASSERT_EQ(1, shaping.positionedGlyphs.count(0)); + ASSERT_EQ(2, shaping.positionedGlyphs[0].size()); + const auto& glyphs = shaping.positionedGlyphs[0]; + ASSERT_FLOAT_EQ(-26.25f, glyphs[0].x); + ASSERT_FLOAT_EQ(-24.0f, glyphs[0].y); + ASSERT_FLOAT_EQ(5.25f, glyphs[1].x); + ASSERT_FLOAT_EQ(-10.0f, glyphs[1].y); + ASSERT_TRUE(shaping.hasBaseline); } }