diff --git a/modules/svg/svg_utils.cpp b/modules/svg/svg_utils.cpp index 2cdaa208b5b7..f915de666b46 100644 --- a/modules/svg/svg_utils.cpp +++ b/modules/svg/svg_utils.cpp @@ -100,6 +100,9 @@ void SVGUtils::add_font_face(const String &family, const void *font_data, int le { MutexLock lock(svg_font_registry_mutex); + if (svg_named_font_faces.has(family)) { + return; + } svg_named_font_faces.insert(family, font_bytes); svg_font_registry_serial++; } diff --git a/thirdparty/lunasvg/include/plutovg.h b/thirdparty/lunasvg/include/plutovg.h index 2c318aa72545..b268d45b708c 100644 --- a/thirdparty/lunasvg/include/plutovg.h +++ b/thirdparty/lunasvg/include/plutovg.h @@ -849,6 +849,18 @@ PLUTOVG_API void plutovg_font_face_get_metrics(const plutovg_font_face_t* face, */ PLUTOVG_API void plutovg_font_face_get_glyph_metrics(plutovg_font_face_t* face, float size, plutovg_codepoint_t codepoint, float* advance_width, float* left_side_bearing, plutovg_rect_t* extents); +/** + * @brief Retrieves embedded SVG glyph data for a specified codepoint when available. + * + * @param face A pointer to a `plutovg_font_face_t` object. + * @param codepoint The Unicode code point of the glyph. + * @param svg Pointer that receives the embedded SVG document owned by the font face. + * The returned pointer refers to the font face's internal storage, remains valid only for the + * lifetime of `face`, and must not be modified or freed by the caller. + * @return The SVG document length in bytes, or 0 if no embedded SVG glyph exists. + */ +PLUTOVG_API int plutovg_font_face_get_glyph_svg(plutovg_font_face_t* face, plutovg_codepoint_t codepoint, const char** svg); + /** * @brief Retrieves the path of a glyph and its advance width. * diff --git a/thirdparty/lunasvg/source/plutovg-font.c b/thirdparty/lunasvg/source/plutovg-font.c index 2e67033cb4b3..7e2b8b367b65 100644 --- a/thirdparty/lunasvg/source/plutovg-font.c +++ b/thirdparty/lunasvg/source/plutovg-font.c @@ -256,11 +256,15 @@ static glyph_t* plutovg_font_face_get_glyph(plutovg_font_face_t* face, plutovg_c unsigned int msb = (codepoint >> 8) & 0xFF; if(face->glyphs[msb] == NULL) { face->glyphs[msb] = calloc(GLYPH_CACHE_SIZE, sizeof(glyph_t*)); + if(face->glyphs[msb] == NULL) + return NULL; } unsigned int lsb = codepoint & 0xFF; if(face->glyphs[msb][lsb] == NULL) { glyph_t* glyph = malloc(sizeof(glyph_t)); + if(glyph == NULL) + return NULL; glyph->index = stbtt_FindGlyphIndex(&face->info, codepoint); glyph->nvertices = stbtt_GetGlyphShape(&face->info, glyph->index, &glyph->vertices); stbtt_GetGlyphHMetrics(&face->info, glyph->index, &glyph->advance_width, &glyph->left_side_bearing); @@ -276,6 +280,13 @@ void plutovg_font_face_get_glyph_metrics(plutovg_font_face_t* face, float size, { float scale = plutovg_font_face_get_scale(face, size); glyph_t* glyph = plutovg_font_face_get_glyph(face, codepoint); + if(glyph == NULL) { + if(advance_width) *advance_width = 0.f; + if(left_side_bearing) *left_side_bearing = 0.f; + if(extents) + extents->x = extents->y = extents->w = extents->h = 0.f; + return; + } if(advance_width) *advance_width = glyph->advance_width * scale; if(left_side_bearing) *left_side_bearing = glyph->left_side_bearing * scale; if(extents) { @@ -286,6 +297,18 @@ void plutovg_font_face_get_glyph_metrics(plutovg_font_face_t* face, float size, } } +int plutovg_font_face_get_glyph_svg(plutovg_font_face_t* face, plutovg_codepoint_t codepoint, const char** svg) +{ + if(svg) + *svg = NULL; + if(face == NULL || svg == NULL) + return 0; + glyph_t* glyph = plutovg_font_face_get_glyph(face, codepoint); + if(glyph == NULL) + return 0; + return stbtt_GetGlyphSVG(&face->info, glyph->index, svg); +} + static void glyph_traverse_func(void* closure, plutovg_path_command_t command, const plutovg_point_t* points, int npoints) { plutovg_path_t* path = (plutovg_path_t*)(closure); @@ -319,6 +342,8 @@ float plutovg_font_face_traverse_glyph_path(plutovg_font_face_t* face, float siz plutovg_point_t points[3]; plutovg_point_t current_point = {0, 0}; glyph_t* glyph = plutovg_font_face_get_glyph(face, codepoint); + if(glyph == NULL) + return 0.f; for(int i = 0; i < glyph->nvertices; i++) { switch(glyph->vertices[i].type) { case STBTT_vmove: diff --git a/thirdparty/lunasvg/source/svgtextelement.cpp b/thirdparty/lunasvg/source/svgtextelement.cpp index b6da9ba0c24d..f5a33a59cd00 100644 --- a/thirdparty/lunasvg/source/svgtextelement.cpp +++ b/thirdparty/lunasvg/source/svgtextelement.cpp @@ -1,11 +1,217 @@ #include "svgtextelement.h" #include "svglayoutstate.h" #include "svgrenderstate.h" +#include #include +#include namespace lunasvg { +namespace { + +constexpr std::string_view kSPXDefaultFontFamily = "SPX Default"; +constexpr std::string_view kSPXSymbolsFontFamily = "Symbols"; +constexpr std::string_view kSPXEmojiFontFamily = "Emoji"; + +enum class TextRunKind : uint8_t { + Base, + DefaultFallback, + SymbolsFallback, + Emoji +}; + +static bool isWhitespaceCodepoint(uint32_t codepoint) +{ + switch(codepoint) { + case 0x0009: + case 0x000A: + case 0x000B: + case 0x000C: + case 0x000D: + case 0x0020: + case 0x0085: + case 0x00A0: + case 0x1680: + case 0x2028: + case 0x2029: + case 0x202F: + case 0x205F: + case 0x3000: + return true; + default: + break; + } + + return (codepoint >= 0x2000 && codepoint <= 0x200A); +} + +static bool isEmojiCodepoint(uint32_t codepoint) +{ + if(codepoint == 0x00A9 || codepoint == 0x00AE || codepoint == 0x203C || codepoint == 0x2049 || + codepoint == 0x2122 || codepoint == 0x2139 || codepoint == 0x3030 || codepoint == 0x303D || + codepoint == 0x3297 || codepoint == 0x3299) { + return true; + } + + return (codepoint >= 0x2194 && codepoint <= 0x21AA) || + (codepoint >= 0x231A && codepoint <= 0x27BF) || + (codepoint >= 0x2934 && codepoint <= 0x2935) || + (codepoint >= 0x2B05 && codepoint <= 0x2B55) || + (codepoint >= 0x1F000 && codepoint <= 0x1FAFF) || + (codepoint >= 0x1FC00 && codepoint <= 0x1FFFD); +} + +static bool isEmojiFormattingCodepoint(uint32_t codepoint) +{ + if(codepoint == 0x200D || codepoint == 0x20E3 || codepoint == 0xFE0E || codepoint == 0xFE0F) + return true; + if(codepoint >= 0xE0020 && codepoint <= 0xE007F) + return true; + return codepoint >= 0x1F3FB && codepoint <= 0x1F3FF; +} + +static bool promotesEmojiPresentation(uint32_t codepoint) +{ + return codepoint != 0xFE0E && isEmojiFormattingCodepoint(codepoint); +} + +static bool isDefaultEmojiPresentationCodepoint(uint32_t codepoint) +{ + return (codepoint >= 0x231A && codepoint <= 0x231B) || + (codepoint >= 0x23E9 && codepoint <= 0x23EC) || + codepoint == 0x23F0 || + codepoint == 0x23F3 || + (codepoint >= 0x25FD && codepoint <= 0x25FE) || + (codepoint >= 0x2614 && codepoint <= 0x2615) || + (codepoint >= 0x2648 && codepoint <= 0x2653) || + codepoint == 0x267F || + codepoint == 0x2693 || + codepoint == 0x26A1 || + (codepoint >= 0x26AA && codepoint <= 0x26AB) || + (codepoint >= 0x26BD && codepoint <= 0x26BE) || + (codepoint >= 0x26C4 && codepoint <= 0x26C5) || + codepoint == 0x26CE || + codepoint == 0x26D4 || + codepoint == 0x26EA || + (codepoint >= 0x26F2 && codepoint <= 0x26F3) || + codepoint == 0x26F5 || + codepoint == 0x26FA || + codepoint == 0x26FD || + codepoint == 0x2705 || + (codepoint >= 0x270A && codepoint <= 0x270B) || + codepoint == 0x2728 || + codepoint == 0x274C || + codepoint == 0x274E || + (codepoint >= 0x2753 && codepoint <= 0x2755) || + codepoint == 0x2757 || + (codepoint >= 0x2795 && codepoint <= 0x2797) || + codepoint == 0x27B0 || + codepoint == 0x27BF || + (codepoint >= 0x2B1B && codepoint <= 0x2B1C) || + codepoint == 0x2B50 || + codepoint == 0x2B55; +} + +static bool isSymbolsFallbackCodepoint(uint32_t codepoint) +{ + return (codepoint >= 0x2190 && codepoint <= 0x21FF) || + (codepoint >= 0x2300 && codepoint <= 0x23FF) || + (codepoint >= 0x2460 && codepoint <= 0x27BF) || + (codepoint >= 0x2900 && codepoint <= 0x2BFF); +} + +static bool isDefaultFallbackCodepoint(uint32_t codepoint) +{ + if(codepoint < 0x80 || isEmojiCodepoint(codepoint)) + return false; + if(isWhitespaceCodepoint(codepoint)) + return false; + + return (codepoint >= 0x3000 && codepoint <= 0x303F) || + (codepoint >= 0x3040 && codepoint <= 0x30FF) || + (codepoint >= 0x3100 && codepoint <= 0x312F) || + (codepoint >= 0x31A0 && codepoint <= 0x31BF) || + (codepoint >= 0x31C0 && codepoint <= 0x31EF) || + (codepoint >= 0x3200 && codepoint <= 0x33FF) || + (codepoint >= 0x3400 && codepoint <= 0x4DBF) || + (codepoint >= 0x4E00 && codepoint <= 0x9FFF) || + (codepoint >= 0xA960 && codepoint <= 0xA97F) || + (codepoint >= 0xAC00 && codepoint <= 0xD7AF) || + (codepoint >= 0xD7B0 && codepoint <= 0xD7FF) || + (codepoint >= 0xF900 && codepoint <= 0xFAFF) || + (codepoint >= 0xFE30 && codepoint <= 0xFE6F) || + (codepoint >= 0xFF00 && codepoint <= 0xFFEF) || + (codepoint >= 0x20000 && codepoint <= 0x323AF); +} + +static TextRunKind classifyTextRunKind(uint32_t codepoint, TextRunKind previous, bool emojiPresentation) +{ + if(emojiPresentation || isDefaultEmojiPresentationCodepoint(codepoint)) + return TextRunKind::Emoji; + if(isSymbolsFallbackCodepoint(codepoint)) + return TextRunKind::SymbolsFallback; + if(isEmojiCodepoint(codepoint)) + return TextRunKind::Emoji; + if(isDefaultFallbackCodepoint(codepoint)) + return TextRunKind::DefaultFallback; + if(isWhitespaceCodepoint(codepoint)) + return previous; + return TextRunKind::Base; +} + +static Font resolveFragmentFont(const SVGTextPositioningElement* element, TextRunKind kind) +{ + if(kind == TextRunKind::Base) + return element->font(); + + const auto family = kind == TextRunKind::Emoji + ? kSPXEmojiFontFamily + : kind == TextRunKind::SymbolsFallback ? kSPXSymbolsFontFamily : kSPXDefaultFontFamily; + auto face = fontFaceCache()->getFontFace(family, false, false); + if(face.isNull()) + return element->font(); + return Font(face, element->font().size()); +} + +static bool tryRenderColorEmojiGlyph(const SVGTextFragment& fragment, const Font& font, const Point& origin, const Transform& transform, SVGRenderState& state) +{ + if(fragment.text.size() != 1) + return false; + auto face = font.face().get(); + if(face == nullptr) + return false; + + const char* svgData = nullptr; + auto svgLength = plutovg_font_face_get_glyph_svg(face, fragment.text.front(), &svgData); + if(svgLength <= 0 || svgData == nullptr) + return false; + + plutovg_rect_t glyphExtents = {0}; + plutovg_font_face_get_glyph_metrics(face, font.size(), fragment.text.front(), nullptr, nullptr, &glyphExtents); + auto dstRect = Rect(origin.x + glyphExtents.x, origin.y + glyphExtents.y, glyphExtents.w, glyphExtents.h); + if(dstRect.isEmpty()) + return false; + + // TODO: Cache rendered emoji bitmaps per glyph and size instead of reparsing SVG every frame. + auto document = Document::loadFromData(svgData, static_cast(svgLength)); + if(!document) + return false; + auto bounds = Rect(document->boundingBox()); + if(bounds.isEmpty()) + return false; + + auto bitmapWidth = std::max(1, static_cast(std::ceil(bounds.w))); + auto bitmapHeight = std::max(1, static_cast(std::ceil(bounds.h))); + Bitmap bitmap(bitmapWidth, bitmapHeight); + bitmap.clear(0x00000000); + document->render(bitmap, Matrix::translated(-bounds.x, -bounds.y)); + state->drawImage(bitmap, dstRect, Rect(0, 0, bitmap.width(), bitmap.height()), transform); + return true; +} + +} // namespace + inline const SVGTextNode* toSVGTextNode(const SVGNode* node) { assert(node && node->isTextNode()); @@ -149,11 +355,16 @@ void SVGTextFragmentsBuilder::build(const SVGTextElement* textElement) continue; auto element = toSVGTextPositioningElement(textPosition.node->parent()); SVGTextFragment fragment(element); - auto recordTextFragment = [&](auto startOffset, auto endOffset) { + auto currentRunKind = TextRunKind::Base; + auto recordTextFragment = [&](auto startOffset, auto endOffset, TextRunKind runKind) { + if(startOffset == endOffset) + return; auto text = wholeText.substr(startOffset, endOffset - startOffset); fragment.offset = startOffset; fragment.length = endOffset - startOffset; - fragment.width = element->font().measureText(text); + fragment.font = resolveFragmentFont(element, runKind); + fragment.text.assign(text.begin(), text.end()); + fragment.width = fragment.font.measureText(fragment.text); m_fragments.push_back(fragment); m_x += fragment.width; }; @@ -164,6 +375,7 @@ void SVGTextFragmentsBuilder::build(const SVGTextElement* textElement) auto didStartTextFragment = false; auto lastAngle = 0.f; while(textOffset < textPosition.endOffset) { + auto runKind = classifyTextRunKind(m_text[textOffset], currentRunKind, m_emojiPresentationOffsets.count(textOffset) != 0); SVGCharacterPosition characterPosition; if(m_characterPositions.count(m_characterOffset) > 0) { characterPosition = m_characterPositions.at(m_characterOffset); @@ -173,9 +385,9 @@ void SVGTextFragmentsBuilder::build(const SVGTextElement* textElement) auto dx = characterPosition.dx.value_or(0); auto dy = characterPosition.dy.value_or(0); - auto shouldStartNewFragment = characterPosition.x || characterPosition.y || dx || dy || angle || angle != lastAngle; + auto shouldStartNewFragment = characterPosition.x || characterPosition.y || dx || dy || angle || angle != lastAngle || runKind != currentRunKind || runKind == TextRunKind::Emoji; if(shouldStartNewFragment && didStartTextFragment) { - recordTextFragment(startOffset, textOffset); + recordTextFragment(startOffset, textOffset, currentRunKind); startOffset = textOffset; } @@ -188,6 +400,7 @@ void SVGTextFragmentsBuilder::build(const SVGTextElement* textElement) fragment.angle = angle; fragment.startsNewTextChunk = startsNewTextChunk; didStartTextFragment = true; + currentRunKind = runKind; } lastAngle = angle; @@ -195,7 +408,7 @@ void SVGTextFragmentsBuilder::build(const SVGTextElement* textElement) ++m_characterOffset; } - recordTextFragment(startOffset, textOffset); + recordTextFragment(startOffset, textOffset, currentRunKind); } auto handleTextChunk = [](auto begin, auto end) { @@ -250,6 +463,11 @@ void SVGTextFragmentsBuilder::handleText(const SVGTextNode* node) plutovg_text_iterator_init(&it, text.data(), text.length(), PLUTOVG_TEXT_ENCODING_UTF8); while(plutovg_text_iterator_has_next(&it)) { auto currentCharacter = plutovg_text_iterator_next(&it); + if(isEmojiFormattingCodepoint(currentCharacter)) { + if(promotesEmojiPresentation(currentCharacter) && !m_text.empty()) + m_emojiPresentationOffsets.insert(m_text.length() - 1); + continue; + } if(currentCharacter == '\t' || currentCharacter == '\n' || currentCharacter == '\r') currentCharacter = ' '; if(currentCharacter == ' ' && lastCharacter == ' ' && element->white_space() == WhiteSpace::Default) @@ -409,23 +627,23 @@ void SVGTextElement::render(SVGRenderState& state) const newState->setColor(Color::White); } - std::u32string_view wholeText(m_text); for(const auto& fragment : m_fragments) { auto transform = newState.currentTransform() * Transform::rotated(fragment.angle, fragment.x, fragment.y); - auto text = wholeText.substr(fragment.offset, fragment.length); auto origin = Point(fragment.x, fragment.y); - const auto& font = fragment.element->font(); + const auto& font = fragment.font.isNull() ? fragment.element->font() : fragment.font; if(newState.mode() == SVGRenderMode::Clipping) { - newState->fillText(text, font, origin, transform); + newState->fillText(fragment.text, font, origin, transform); } else { + if(tryRenderColorEmojiGlyph(fragment, font, origin, transform, newState)) + continue; const auto& fill = fragment.element->fill(); const auto& stroke = fragment.element->stroke(); auto stroke_width = fragment.element->stroke_width(); if(fill.applyPaint(newState)) - newState->fillText(text, font, origin, transform); + newState->fillText(fragment.text, font, origin, transform); if(stroke.applyPaint(newState)) { - newState->strokeText(text, stroke_width, font, origin, transform); + newState->strokeText(fragment.text, stroke_width, font, origin, transform); } } } @@ -437,10 +655,19 @@ Rect SVGTextElement::boundingBox(bool includeStroke) const { auto boundingBox = Rect::Invalid; for(const auto& fragment : m_fragments) { - const auto& font = fragment.element->font(); + const auto& font = fragment.font.isNull() ? fragment.element->font() : fragment.font; const auto& stroke = fragment.element->stroke(); auto fragmentTranform = Transform::rotated(fragment.angle, fragment.x, fragment.y); auto fragmentRect = Rect(fragment.x, fragment.y - font.ascent(), fragment.width, fragment.element->font_size()); + if(fragment.text.size() == 1) { + plutovg_rect_t glyphExtents = {0}; + const char* svgData = nullptr; + auto svgLength = plutovg_font_face_get_glyph_svg(font.face().get(), fragment.text.front(), &svgData); + if(svgLength > 0 && svgData != nullptr) { + plutovg_font_face_get_glyph_metrics(font.face().get(), font.size(), fragment.text.front(), nullptr, nullptr, &glyphExtents); + fragmentRect = Rect(fragment.x + glyphExtents.x, fragment.y + glyphExtents.y, glyphExtents.w, glyphExtents.h); + } + } if(includeStroke && stroke.isRenderable()) fragmentRect.inflate(fragment.element->stroke_width() / 2.f); boundingBox.unite(fragmentTranform.mapRect(fragmentRect)); diff --git a/thirdparty/lunasvg/source/svgtextelement.h b/thirdparty/lunasvg/source/svgtextelement.h index 8b0a0e24b8f8..1bdd5981da6e 100644 --- a/thirdparty/lunasvg/source/svgtextelement.h +++ b/thirdparty/lunasvg/source/svgtextelement.h @@ -4,6 +4,7 @@ #include "svgelement.h" #include +#include namespace lunasvg { @@ -35,6 +36,8 @@ using SVGTextPositionList = std::vector; struct SVGTextFragment { explicit SVGTextFragment(const SVGTextPositioningElement* element) : element(element) {} const SVGTextPositioningElement* element; + Font font; + std::u32string text; size_t offset = 0; size_t length = 0; float x = 0; @@ -59,6 +62,7 @@ class SVGTextFragmentsBuilder { std::u32string& m_text; SVGTextFragmentList& m_fragments; SVGCharacterPositions m_characterPositions; + std::set m_emojiPresentationOffsets; SVGTextPositionList m_textPositions; size_t m_characterOffset = 0; float m_x = 0;