From 4e90ef831bae323592acb11c06dd1db1d0edb22d Mon Sep 17 00:00:00 2001 From: reuk Date: Wed, 28 Feb 2024 19:28:45 +0000 Subject: [PATCH] Typeface: Add support for rendering COLRv0 glyphs and png-based glyphs This is sufficient for initial support of the system emoji fonts on each platform: - Noto Color Emoji on Linux and Android, png-based - Apple Color Emoji on macOS and iOS, png-based - Segoe UI Emoji on Windows 10 and 11, COLRv0-based - This font also provides COLRv1 support, at least on Windows 11, but JUCE will ignore that and use the COLRv0 data instead --- modules/juce_graphics/fonts/juce_Typeface.cpp | 415 +++++++++++++++++- modules/juce_graphics/fonts/juce_Typeface.h | 55 ++- modules/juce_graphics/juce_graphics.h | 6 +- .../native/juce_RenderingHelpers.h | 77 ++-- 4 files changed, 506 insertions(+), 47 deletions(-) diff --git a/modules/juce_graphics/fonts/juce_Typeface.cpp b/modules/juce_graphics/fonts/juce_Typeface.cpp index 06dfb7f155..9aacdae335 100644 --- a/modules/juce_graphics/fonts/juce_Typeface.cpp +++ b/modules/juce_graphics/fonts/juce_Typeface.cpp @@ -246,28 +246,25 @@ static HbDrawFuncs getPathDrawFuncs() return funcs; } -static Path getTypefaceGlyph (const Typeface& typeface, int glyphNumber) +[[nodiscard]] static Path getGlyphPathInGlyphUnits (hb_codepoint_t glyph, hb_font_t* font) { static const auto funcs = getPathDrawFuncs(); - auto* font = typeface.getNativeDetails().getFont(); - Path result; - hb_font_draw_glyph (font, (hb_codepoint_t) glyphNumber, funcs.get(), &result); - - // Convert to em units - result.applyTransform (AffineTransform::scale (1.0f / (float) hb_face_get_upem (hb_font_get_face (font))).scaled (1.0f, -1.0f)); - + hb_font_draw_glyph (font, glyph, funcs.get(), &result); return result; } void Typeface::getOutlineForGlyph (int glyphNumber, Path& path) { + const auto native = getNativeDetails(); + auto* font = native.getFont(); const auto metrics = getNativeDetails().getLegacyMetrics(); + const auto scale = metrics.getHeightToPointsFactor() / (float) hb_face_get_upem (hb_font_get_face (font)); // getTypefaceGlyph returns glyphs in em space, getOutlineForGlyph returns glyphs in "special JUCE units" space - path = getTypefaceGlyph (*this, glyphNumber); - path.applyTransform (AffineTransform::scale (metrics.getHeightToPointsFactor())); + path = getGlyphPathInGlyphUnits ((hb_codepoint_t) glyphNumber, getNativeDetails().getFont()); + path.applyTransform (AffineTransform::scale (scale, -scale)); } void Typeface::applyVerticalHintingTransform (float, Path&) @@ -279,12 +276,402 @@ EdgeTable* Typeface::getEdgeTableForGlyph (int glyphNumber, const AffineTransfor { Path path; getOutlineForGlyph (glyphNumber, path); + path.applyTransform (transform); - if (path.isEmpty()) - return nullptr; + return new EdgeTable (path.getBounds().getSmallestIntegerContainer().expanded (1, 0), std::move (path), {}); +} - return new EdgeTable (path.getBoundsTransformed (transform).getSmallestIntegerContainer().expanded (1, 0), - path, transform); +static Colour makeColour (hb_color_t c) +{ + return PixelARGB (hb_color_get_alpha (c), + hb_color_get_red (c), + hb_color_get_green (c), + hb_color_get_blue (c)); +} + +class HbPaintGroup +{ +public: + void pushClipGlyph (const AffineTransform& t, hb_codepoint_t glyph, hb_font_t* font) + { + auto path = getGlyphPathInGlyphUnits (glyph, font); + path.applyTransform (t); + pushClip (std::move (path)); + } + + void pushClipRect (const AffineTransform& t, Rectangle rect) + { + Path path; + path.addRectangle (rect); + path.applyTransform (t); + pushClip (std::move (path)); + } + + void popClip() + { + clip.pop_back(); + } + + void fill (hb_bool_t foreground, hb_color_t c) + { + addLayerChecked (foreground, c); + } + + void linearGradient (hb_color_line_t&, Point, Point, Point) + { + // Support for COLRv1 glyphs is not fully implemented. + jassertfalse; + } + + void radialGradient (hb_color_line_t&, Point, float, Point, float) + { + // Support for COLRv1 glyphs is not fully implemented. + jassertfalse; + } + + void sweepGradient (hb_color_line_t&, Point, float, float) + { + // Support for COLRv1 glyphs is not fully implemented. + jassertfalse; + } + + bool image (const AffineTransform& t, hb_blob_t* image, unsigned int width, unsigned int height, hb_tag_t format, float, hb_glyph_extents_t* extents) + { + switch (format) + { + case HB_PAINT_IMAGE_FORMAT_BGRA: + // Raw bitmap-based glyphs are not currently supported. + // If you hit this assertion, please let the JUCE team know which font you're + // attempting to use. + // Depending on demand, support for this feature may be added in the future. + jassertfalse; + return false; + + case HB_PAINT_IMAGE_FORMAT_PNG: + { + unsigned int imageDataSize{}; + const char* imageData = hb_blob_get_data (image, &imageDataSize); + const auto juceImage = PNGImageFormat::loadFrom (imageData, imageDataSize); + + if (juceImage.isNull()) + return false; + + const auto transform = AffineTransform::scale ((float) extents->width / (float) width, + (float) extents->height / (float) height) + .translated ((float) extents->x_bearing, + (float) extents->y_bearing) + .followedBy (t); + ImageLayer imageLayer { juceImage, transform }; + layers.push_back ({ std::move (imageLayer) }); + return true; + } + + case HB_PAINT_IMAGE_FORMAT_SVG: + // SVG-based glyphs are not currently supported. + // If you hit this assertion, please let the JUCE team know which font you're + // attempting to use. + // Depending on demand, support for this feature may be added in the future. + jassertfalse; + return false; + } + + jassertfalse; + return false; + } + + std::vector getLayers() && + { + return std::move (layers); + } + + void appendLayers (Span l) + { + for (auto& layer : l) + layers.emplace_back (std::move (layer)); + } + +private: + GlyphLayer makeLayer (hb_bool_t foreground, hb_color_t c) const + { + return { ColourLayer { clip.back(), foreground ? std::optional() : makeColour (c) } }; + } + + void pushClip (Path path) + { + pushClip ({ path.getBounds().getSmallestIntegerContainer().expanded (1, 0), path, {} }); + } + + template + void addLayerChecked (Args&&... args) + { + if (clip.empty()) + { + jassertfalse; + return; + } + + layers.push_back (makeLayer (std::forward (args)...)); + } + + void pushClip (const EdgeTable& et) + { + if (! clip.empty()) + { + clip.push_back (clip.back()); + clip.back().clipToEdgeTable (et); + } + else + { + clip.push_back (et); + } + } + + std::vector clip; + std::vector layers; +}; + +class HbPaintContext +{ +public: + explicit HbPaintContext (const AffineTransform& transformIn) + : baseTransform (transformIn) + { + } + + void addTransform (const AffineTransform& transform) + { + transforms.push_back (transforms.empty() ? transform : transform.followedBy (transforms.back())); + } + + void popTransform() + { + transforms.pop_back(); + } + + void pushClipGlyph (hb_codepoint_t glyph, hb_font_t* font) + { + groups.back().pushClipGlyph (getTransform(), glyph, font); + } + + void pushClipRect (Rectangle rect) + { + groups.back().pushClipRect (getTransform(), rect); + } + + void popClip() + { + groups.back().popClip(); + } + + void fill (hb_bool_t foreground, hb_color_t c) + { + groups.back().fill (foreground, c); + } + + void linearGradient (hb_color_line_t& line, Point p0, Point p1, Point p2) + { + groups.back().linearGradient (line, p0, p1, p2); + } + + void radialGradient (hb_color_line_t& line, Point p0, float r0, Point p1, float r1) + { + groups.back().radialGradient (line, p0, r0, p1, r1); + } + + void sweepGradient (hb_color_line_t& line, Point p, float begin, float end) + { + groups.back().sweepGradient (line, p, begin, end); + } + + bool image (hb_blob_t* image, unsigned int width, unsigned int height, hb_tag_t format, float slant, hb_glyph_extents_t* extents) + { + return groups.back().image (getTransform(), image, width, height, format, slant, extents); + } + + void pushGroup() + { + groups.emplace_back(); + } + + void popGroup ([[maybe_unused]] hb_paint_composite_mode_t mode) + { + // There is currently extremely limited support for colour glyph blend modes + jassert (mode == HB_PAINT_COMPOSITE_MODE_SRC_OVER); + + auto newLayers = std::move (groups.back()).getLayers(); + groups.pop_back(); + groups.back().appendLayers (newLayers); + } + + std::vector getLayers() && + { + return std::move (groups.back()).getLayers(); + } + +private: + AffineTransform getTransform() const + { + const auto glyphSpaceTransform = transforms.empty() ? AffineTransform{} : transforms.back(); + return glyphSpaceTransform.followedBy (baseTransform); + } + + AffineTransform baseTransform; + std::vector transforms; + std::vector groups = std::vector (1); +}; + +using HbPaintFuncs = std::unique_ptr>; + +static HbPaintFuncs getPathPaintFuncs() +{ + HbPaintFuncs funcs { hb_paint_funcs_create() }; + + hb_paint_funcs_set_push_transform_func (funcs.get(), [] (auto*, auto* data, auto xx, auto yx, auto xy, auto yy, auto dx, auto dy, auto*) + { + auto& context = *static_cast (data); + context.addTransform ({ xx, xy, dx, yx, yy, dy }); + }, nullptr, nullptr); + + hb_paint_funcs_set_pop_transform_func (funcs.get(), [] (auto*, void* data, auto*) + { + auto& context = *static_cast (data); + context.popTransform(); + }, nullptr, nullptr); + + hb_paint_funcs_set_push_clip_glyph_func (funcs.get(), [] (auto*, void* data, auto glyph, auto* font, auto*) + { + auto& context = *static_cast (data); + context.pushClipGlyph (glyph, font); + }, nullptr, nullptr); + + hb_paint_funcs_set_push_clip_rectangle_func (funcs.get(), [] (auto*, void* data, auto xmin, auto ymin, auto xmax, auto ymax, auto*) + { + auto& context = *static_cast (data); + context.pushClipRect (Rectangle::leftTopRightBottom (xmin, ymin, xmax, ymax)); + }, nullptr, nullptr); + + hb_paint_funcs_set_pop_clip_func (funcs.get(), [] (auto*, void* data, auto*) + { + auto& context = *static_cast (data); + context.popClip(); + }, nullptr, nullptr); + + hb_paint_funcs_set_color_func (funcs.get(), [] (auto*, void* data, auto foreground, auto colour, auto*) + { + auto& context = *static_cast (data); + context.fill (foreground, colour); + }, nullptr, nullptr); + + hb_paint_funcs_set_image_func (funcs.get(), [] (auto*, void* data, auto* image, auto w, auto h, auto format, auto slant, auto* extents, auto*) -> hb_bool_t + { + auto& context = *static_cast (data); + return context.image (image, w, h, format, slant, extents); + }, nullptr, nullptr); + + hb_paint_funcs_set_linear_gradient_func (funcs.get(), [] (auto*, auto* data, auto* colourLine, auto x0, auto y0, auto x1, auto y1, auto x2, auto y2, auto*) + { + auto& context = *static_cast (data); + context.linearGradient (*colourLine, { x0, y0 }, { x1, y1 }, { x2, y2 }); + }, nullptr, nullptr); + + hb_paint_funcs_set_radial_gradient_func (funcs.get(), [] (auto*, auto* data, auto* colourLine, auto x0, auto y0, auto r0, auto x1, auto y1, auto r1, auto*) + { + auto& context = *static_cast (data); + context.radialGradient (*colourLine, { x0, y0 }, r0, { x1, y1 }, r1); + }, nullptr, nullptr); + + hb_paint_funcs_set_sweep_gradient_func (funcs.get(), [] (auto*, auto* data, auto* colourLine, auto x0, auto y0, auto begin, auto end, auto*) + { + auto& context = *static_cast (data); + context.sweepGradient (*colourLine, { x0, y0 }, begin, end); + }, nullptr, nullptr); + + hb_paint_funcs_set_push_group_func (funcs.get(), [] (auto*, auto* data, auto*) + { + auto& context = *static_cast (data); + context.pushGroup(); + }, nullptr, nullptr); + + hb_paint_funcs_set_pop_group_func (funcs.get(), [] (auto*, auto* data, auto mode, auto*) + { + auto& context = *static_cast (data); + context.popGroup (mode); + }, nullptr, nullptr); + + hb_paint_funcs_set_custom_palette_color_func (funcs.get(), [] (auto*, auto*, auto, auto*, auto*) -> hb_bool_t + { + return false; + }, nullptr, nullptr); + + return funcs; +} + +static std::vector getCOLRv0Layers (const Typeface& typeface, int glyphNumber, const AffineTransform& transform) +{ + auto* font = typeface.getNativeDetails().getFont(); + auto* face = hb_font_get_face (font); + constexpr auto palette = 0; + + auto numLayers = hb_ot_color_glyph_get_layers (face, (hb_codepoint_t) glyphNumber, 0, nullptr, nullptr); + std::vector layers (numLayers); + hb_ot_color_glyph_get_layers (face, (hb_codepoint_t) glyphNumber, 0, &numLayers, layers.data()); + + if (layers.empty()) + return {}; + + std::vector result; + + for (const auto& layer : layers) + { + const auto hbFillColour = layer.color_index == 0xffff ? std::optional() : [&] + { + hb_color_t colour{}; + unsigned int numColours = 1; + hb_ot_color_palette_get_colors (face, palette, layer.color_index, &numColours, &colour); + return colour; + }(); + + const auto juceFillColour = hbFillColour.has_value() ? makeColour (*hbFillColour) : std::optional(); + + auto path = getGlyphPathInGlyphUnits (layer.glyph, font); + path.applyTransform (transform); + result.push_back ({ ColourLayer + { + EdgeTable { path.getBounds().getSmallestIntegerContainer().expanded (1, 0), path, {} }, + juceFillColour + } }); + } + + return result; +} + +std::vector Typeface::getLayersForGlyph (int glyphNumber, const AffineTransform& transform, float) const +{ + auto* font = getNativeDetails().getFont(); + const auto metrics = getNativeDetails().getLegacyMetrics(); + const auto scale = metrics.getHeightToPointsFactor() / (float) hb_face_get_upem (hb_font_get_face (font)); + const auto combinedTransform = AffineTransform::scale (scale, -scale).followedBy (transform); + + // Before calling through to the 'paint' API, which JUCE can't easily support due to complex + // gradients and blend modes, attempt to load COLRv0 layers for the glyph, which we'll be able + // to render more successfully. + auto basicLayers = getCOLRv0Layers (*this, glyphNumber, combinedTransform); + + if (! basicLayers.empty()) + return basicLayers; + + constexpr auto palette = 0; + + static const auto funcs = getPathPaintFuncs(); + + HbPaintContext context { combinedTransform }; + hb_font_paint_glyph (font, + (hb_codepoint_t) glyphNumber, + funcs.get(), + &context, + palette, + {}); + return std::move (context).getLayers(); } float Typeface::getAscent() const { return getNativeDetails().getLegacyMetrics().getScaledAscent(); } diff --git a/modules/juce_graphics/fonts/juce_Typeface.h b/modules/juce_graphics/fonts/juce_Typeface.h index 0f738bf5d2..c8a2f575d7 100644 --- a/modules/juce_graphics/fonts/juce_Typeface.h +++ b/modules/juce_graphics/fonts/juce_Typeface.h @@ -35,6 +35,30 @@ namespace juce { +/** A single path-based layer of a colour glyph. Contains the glyph shape and the colour in which + the shape should be painted. +*/ +struct ColourLayer +{ + EdgeTable clip; + std::optional colour; ///< nullopt indicates 'foreground' +}; + +/** A bitmap representing (part of) a glyph, most commonly used to represent colour emoji glyphs. +*/ +struct ImageLayer +{ + Image image; + AffineTransform transform; +}; + +/** A single layer that makes up part of a glyph image. +*/ +struct GlyphLayer +{ + std::variant layer; +}; + //============================================================================== /** A typeface represents a size-independent font. @@ -166,8 +190,35 @@ public: */ void getOutlineForGlyph (int glyphNumber, Path& path); - /** Returns a new EdgeTable that contains the path for the given glyph, with the specified transform applied. */ - EdgeTable* getEdgeTableForGlyph (int glyphNumber, const AffineTransform& transform, float fontHeight); + /** @deprecated + + Returns a new EdgeTable that contains the path for the given glyph, with the specified transform applied. + + This is only capable of returning monochromatic glyphs. In fonts that contain multiple glyph + styles with fallbacks (COLRv1, COLRv0, monochromatic), this will always return the + monochromatic variant. + + The height is specified in JUCE font-height units. + + getLayersForGlyph() has better support for multilayer and bitmap glyphs, so it should be + preferred in new code. + */ + [[deprecated ("Prefer getLayersForGlyph")]] + EdgeTable* getEdgeTableForGlyph (int glyphNumber, const AffineTransform& transform, float normalisedHeight); + + /** Returns the layers that should be painted in order to display this glyph. + + Layers should be painted in the same order as they are returned, i.e. layer[0], layer[1] etc. + + This should generally be preferred to getEdgeTableForGlyph, as it is more flexible. + Currently, this only supports COLRv0 and bitmap fonts (no SVG or COLRv1). + Support for SVG and COLRv1 may be added in the future, depending on demand. However, this + would require significant additions to JUCE's rendering code, so it has been omitted for + now. + + The height is specified in JUCE font-height units. + */ + std::vector getLayersForGlyph (int glyphNumber, const AffineTransform&, float normalisedHeight) const; //============================================================================== /** Changes the number of fonts that are cached in memory. */ diff --git a/modules/juce_graphics/juce_graphics.h b/modules/juce_graphics/juce_graphics.h index 524c594b4d..3054e337eb 100644 --- a/modules/juce_graphics/juce_graphics.h +++ b/modules/juce_graphics/juce_graphics.h @@ -140,16 +140,16 @@ namespace juce #include "images/juce_ImageCache.h" #include "images/juce_ImageConvolutionKernel.h" #include "images/juce_ImageFileFormat.h" +#include "contexts/juce_GraphicsContext.h" +#include "images/juce_Image.h" +#include "colour/juce_FillType.h" #include "fonts/juce_Typeface.h" #include "fonts/juce_Font.h" #include "fonts/juce_AttributedString.h" #include "fonts/juce_GlyphArrangement.h" #include "fonts/juce_TextLayout.h" -#include "contexts/juce_GraphicsContext.h" #include "contexts/juce_LowLevelGraphicsContext.h" -#include "images/juce_Image.h" #include "images/juce_ScaledImage.h" -#include "colour/juce_FillType.h" #include "fonts/juce_LruCache.h" #include "native/juce_RenderingHelpers.h" #include "contexts/juce_LowLevelGraphicsSoftwareRenderer.h" diff --git a/modules/juce_graphics/native/juce_RenderingHelpers.h b/modules/juce_graphics/native/juce_RenderingHelpers.h index a03727d74f..beb0ff1e10 100644 --- a/modules/juce_graphics/native/juce_RenderingHelpers.h +++ b/modules/juce_graphics/native/juce_RenderingHelpers.h @@ -182,22 +182,18 @@ public: cache = {}; } - template - void drawGlyph (RenderTargetType& target, const Font& font, const int glyphNumber, Point pos) + const auto& get (const Font& font, const int glyphNumber) { const ScopedLock sl { lock }; - const auto& table = cache.get (Key { font, glyphNumber }, [] (const auto& key) + return cache.get (Key { font, glyphNumber }, [] (const auto& key) { auto fontHeight = key.font.getHeight(); auto typeface = key.font.getTypefacePtr(); - return rawToUniquePtr (typeface->getEdgeTableForGlyph (key.glyph, - AffineTransform::scale (fontHeight * key.font.getHorizontalScale(), - fontHeight), - fontHeight)); + return typeface->getLayersForGlyph (key.glyph, + AffineTransform::scale (fontHeight * key.font.getHorizontalScale(), + fontHeight), + fontHeight); }); - - if (table != nullptr) - target.fillEdgeTable (*table, pos.x, roundToInt (pos.y)); } private: @@ -218,7 +214,7 @@ private: } }; - LruCache> cache; + LruCache> cache; CriticalSection lock; static GlyphCache*& getSingletonPointer() noexcept @@ -2599,22 +2595,24 @@ public: void setFont (const Font& newFont) override { stack->font = newFont; } const Font& getFont() override { return stack->font; } - void drawGlyph (int glyphNumber, const AffineTransform& trans) override + void drawGlyph (int i, const AffineTransform& t) override { if (stack->clip == nullptr) return; - if (trans.isOnlyTranslation() && ! stack->transform.isRotated) + const auto [layers, drawPosition] = [&] { - auto& cache = RenderingHelpers::GlyphCache::getInstance(); - const Point pos (trans.getTranslationX(), trans.getTranslationY()); + if (t.isOnlyTranslation() && ! stack->transform.isRotated) + { + auto& cache = RenderingHelpers::GlyphCache::getInstance(); + const Point pos (t.getTranslationX(), t.getTranslationY()); + + if (stack->transform.isOnlyTranslated) + { + const auto drawPos = pos + stack->transform.offset.toFloat(); + return std::tuple (cache.get (stack->font, i), drawPos); + } - if (stack->transform.isOnlyTranslated) - { - cache.drawGlyph (*stack, stack->font, glyphNumber, pos + stack->transform.offset.toFloat()); - } - else - { auto f = stack->font; f.setHeight (f.getHeight() * stack->transform.complexTransform.mat11); @@ -2623,18 +2621,41 @@ public: if (std::abs (xScale - 1.0f) > 0.01f) f.setHorizontalScale (xScale); - cache.drawGlyph (*stack, f, glyphNumber, stack->transform.transformed (pos)); + const auto drawPos = stack->transform.transformed (pos); + return std::tuple (cache.get (f, i), drawPos); } - } - else - { + const auto fontHeight = stack->font.getHeight(); const auto fontTransform = AffineTransform::scale (fontHeight * stack->font.getHorizontalScale(), - fontHeight).followedBy (trans); + fontHeight).followedBy (t); const auto fullTransform = stack->transform.getTransformWith (fontTransform); + return std::tuple (stack->font.getTypefacePtr()->getLayersForGlyph (i, fullTransform, fontHeight), Point{}); + }(); - if (auto et = rawToUniquePtr (stack->font.getTypefacePtr()->getEdgeTableForGlyph (glyphNumber, fullTransform, fontHeight))) - stack->fillShape (*new ClipRegions::EdgeTableRegion (*et), false); + const auto initialFill = stack->fillType; + const ScopeGuard scope { [&] { stack->setFillType (initialFill); } }; + + for (const auto& layer : layers) + { + if (auto* colourLayer = std::get_if (&layer.layer)) + { + if (auto fill = colourLayer->colour) + stack->setFillType (*fill); + + stack->fillEdgeTable (colourLayer->clip, drawPosition.x, (int) drawPosition.y); + } + else if (auto* imageLayer = std::get_if (&layer.layer)) + { + // The position arguments to fillEdgeTable are in physical screen-space, + // and do not take the current context transform into account. + // However, drawImage *does* apply the context transform internally. + // We apply the inverse context transform here so that after the + // real context transform is applied, the image will be painted at the + // physical position specified by drawPosition. + const auto imageTransform = imageLayer->transform.translated (drawPosition) + .followedBy (stack->transform.getTransform().inverted()); + stack->drawImage (imageLayer->image, imageTransform); + } } }