From a3d64c7784f1c4ee5f4a328fc44c2b091caa9736 Mon Sep 17 00:00:00 2001 From: reuk Date: Mon, 9 Jun 2025 17:06:52 +0100 Subject: [PATCH] Typeface: Enable advanced colour glyph rendering on Android Android 15+ removed the 'legacy' png-based emoji font. Modern Android versions may include only a COLR-v1-based font, which JUCE cannot render itself. As a workaround, on Android, we use a Canvas object to render each emoji glyph into a bitmap, and then render that bitmap in the same way as a legacy png-based glyph. This won't look as crisp as rendering COLRv1 glyphs directly, especially at larger sizes, but this is a sufficient stop-gap for the time being. --- .../native/juce_JNIHelpers_android.h | 11 +- modules/juce_graphics/fonts/juce_Typeface.cpp | 42 +++- modules/juce_graphics/fonts/juce_Typeface.h | 5 +- .../native/juce_Fonts_android.cpp | 226 +++++++++++++++--- .../native/juce_RenderingHelpers.h | 5 +- 5 files changed, 243 insertions(+), 46 deletions(-) diff --git a/modules/juce_core/native/juce_JNIHelpers_android.h b/modules/juce_core/native/juce_JNIHelpers_android.h index c756b81802..5856ab57ef 100644 --- a/modules/juce_core/native/juce_JNIHelpers_android.h +++ b/modules/juce_core/native/juce_JNIHelpers_android.h @@ -540,6 +540,7 @@ DECLARE_JNI_CLASS (AndroidPackageManager, "android/content/pm/PackageManager") #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ METHOD (constructor, "", "(I)V") \ + METHOD (defaultConstructor, "", "()V") \ METHOD (setColor, "setColor", "(I)V") \ METHOD (setAlpha, "setAlpha", "(I)V") \ METHOD (setTypeface, "setTypeface", "(Landroid/graphics/Typeface;)Landroid/graphics/Typeface;") \ @@ -565,6 +566,12 @@ DECLARE_JNI_CLASS (AndroidPaint, "android/graphics/Paint") DECLARE_JNI_CLASS (AndroidCanvas, "android/graphics/Canvas") #undef JNI_CLASS_MEMBERS +#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ + METHOD (drawGlyphs, "drawGlyphs", "([II[FIILandroid/graphics/fonts/Font;Landroid/graphics/Paint;)V") + + DECLARE_JNI_CLASS_WITH_MIN_SDK (AndroidCanvas31, "android/graphics/Canvas", 31) +#undef JNI_CLASS_MEMBERS + #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ STATICMETHOD (getActivity, "getActivity", "(Landroid/content/Context;ILandroid/content/Intent;I)Landroid/app/PendingIntent;") \ STATICMETHOD (getBroadcast, "getBroadcast", "(Landroid/content/Context;ILandroid/content/Intent;I)Landroid/app/PendingIntent;") \ @@ -706,8 +713,10 @@ DECLARE_JNI_CLASS (JavaBoolean, "java/lang/Boolean") METHOD (remaining, "remaining", "()I") \ METHOD (hasArray, "hasArray", "()Z") \ METHOD (array, "array", "()[B") \ + METHOD (put, "put", "([B)Ljava/nio/ByteBuffer;") \ METHOD (setOrder, "order", "(Ljava/nio/ByteOrder;)Ljava/nio/ByteBuffer;") \ - STATICMETHOD (wrap, "wrap", "([B)Ljava/nio/ByteBuffer;") + STATICMETHOD (wrap, "wrap", "([B)Ljava/nio/ByteBuffer;") \ + STATICMETHOD (allocateDirect, "allocateDirect", "(I)Ljava/nio/ByteBuffer;") \ DECLARE_JNI_CLASS (JavaByteBuffer, "java/nio/ByteBuffer") #undef JNI_CLASS_MEMBERS diff --git a/modules/juce_graphics/fonts/juce_Typeface.cpp b/modules/juce_graphics/fonts/juce_Typeface.cpp index 0c49be92eb..dd87b50126 100644 --- a/modules/juce_graphics/fonts/juce_Typeface.cpp +++ b/modules/juce_graphics/fonts/juce_Typeface.cpp @@ -175,11 +175,21 @@ using HbFace = std::unique_ptr>; using HbBlob = std::unique_ptr>; +struct TypefaceFallbackColourGlyphSupport +{ + virtual ~TypefaceFallbackColourGlyphSupport() = default; + virtual std::vector getFallbackColourGlyphLayers (int, const AffineTransform&) const = 0; +}; + class Typeface::Native { public: - Native (hb_font_t* fontRef, TypefaceAscentDescent nonPortableMetricsIn) - : font (fontRef), nonPortable (nonPortableMetricsIn) + Native (hb_font_t* fontRef, + TypefaceAscentDescent nonPortableMetricsIn, + const TypefaceFallbackColourGlyphSupport* colourGlyphSupportIn = {}) + : font (fontRef), + nonPortable (nonPortableMetricsIn), + colourGlyphSupport (colourGlyphSupportIn) { } @@ -213,6 +223,15 @@ public: return subFont; } + std::vector getFallbackColourGlyphLayers (int glyph, + const AffineTransform& transform) const + { + if (colourGlyphSupport != nullptr) + return colourGlyphSupport->getFallbackColourGlyphLayers (glyph, transform); + + return {}; + } + private: static TypefaceAscentDescent findPortableMetrics (hb_font_t* f, TypefaceAscentDescent fallback) { @@ -235,6 +254,7 @@ private: TypefaceAscentDescent nonPortable; TypefaceAscentDescent portable = findPortableMetrics (font, nonPortable); + const TypefaceFallbackColourGlyphSupport* colourGlyphSupport = nullptr; }; struct FontStyleHelpers @@ -524,10 +544,13 @@ static std::vector getBitmapLayer (const Typeface& typeface, int gly return { GlyphLayer { ImageLayer { juceImage, transform } } }; } -std::vector Typeface::getLayersForGlyph (TypefaceMetricsKind kind, int glyphNumber, const AffineTransform& transform, float) const +std::vector Typeface::getLayersForGlyph (TypefaceMetricsKind kind, + int glyphNumber, + const AffineTransform& transform) const { - auto* font = getNativeDetails().getFont(); - const auto metrics = getNativeDetails().getAscentDescent (kind); + auto native = getNativeDetails(); + auto* font = native.getFont(); + const auto metrics = native.getAscentDescent (kind); const auto factor = metrics.getHeightToPointsFactor(); jassert (! std::isinf (factor)); const auto scale = factor / (float) hb_face_get_upem (hb_font_get_face (font)); @@ -542,7 +565,14 @@ std::vector Typeface::getLayersForGlyph (TypefaceMetricsKind kind, i if (auto layers = getCOLRv0Layers (*this, glyphNumber, combinedTransform); ! layers.empty()) return layers; - // No bitmap or COLRv0 for this glyph, so just get a simple monochromatic outline + // Some fonts (e.g. Noto Color Emoji on Android) might only contain COLRv1 data, which we can't + // easily display. In such cases, we can use system facilities to render the glyph into a + // bitmap. If the face has colour info that wasn't already handled, try rendering to a bitmap. + if (getColourGlyphFormats() != 0) + if (auto layer = native.getFallbackColourGlyphLayers (glyphNumber, combinedTransform); ! layer.empty()) + return layer; + + // No colour info available for this glyph, so just get a simple monochromatic outline auto path = getGlyphPathInGlyphUnits ((hb_codepoint_t) glyphNumber, font); if (path.isEmpty()) diff --git a/modules/juce_graphics/fonts/juce_Typeface.h b/modules/juce_graphics/fonts/juce_Typeface.h index aa95f2513e..9dae78116a 100644 --- a/modules/juce_graphics/fonts/juce_Typeface.h +++ b/modules/juce_graphics/fonts/juce_Typeface.h @@ -255,13 +255,10 @@ public: 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 (TypefaceMetricsKind, int glyphNumber, - const AffineTransform&, - float normalisedHeight) const; + const AffineTransform&) const; /** Kinds of colour glyph format that may be implemented by a particular typeface. Most typefaces are monochromatic, and do not support any colour formats. diff --git a/modules/juce_graphics/native/juce_Fonts_android.cpp b/modules/juce_graphics/native/juce_Fonts_android.cpp index e573dda812..207e482856 100644 --- a/modules/juce_graphics/native/juce_Fonts_android.cpp +++ b/modules/juce_graphics/native/juce_Fonts_android.cpp @@ -61,6 +61,14 @@ Typeface::Ptr Font::Native::getDefaultPlatformTypefaceForFont (const Font& font) DECLARE_JNI_CLASS (TypefaceClass, "android/graphics/Typeface") #undef JNI_CLASS_MEMBERS +#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ + METHOD (create, "", "(Ljava/nio/ByteBuffer;)V") \ + METHOD (setTtcIndex, "setTtcIndex", "(I)Landroid/graphics/fonts/Font$Builder;") \ + METHOD (build, "build", "()Landroid/graphics/fonts/Font;") \ + + DECLARE_JNI_CLASS_WITH_MIN_SDK (AndroidFontBuilder, "android/graphics/fonts/Font$Builder", 29) +#undef JNI_CLASS_MEMBERS + #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ METHOD (constructor, "", "()V") \ METHOD (computeBounds, "computeBounds", "(Landroid/graphics/RectF;Z)V") @@ -98,6 +106,7 @@ std::unique_ptr makeAndroidInputStreamWrapper (LocalRef st struct AndroidCachedTypeface { std::shared_ptr font; + GlobalRef javaFont; TypefaceAscentDescent nonPortableMetrics; }; @@ -226,20 +235,24 @@ StringArray Font::findAllTypefaceStyles (const String& family) } //============================================================================== -class AndroidTypeface final : public Typeface +class AndroidTypeface final : public Typeface, + private TypefaceFallbackColourGlyphSupport { public: - enum class DoCache - { - no, - yes - }; - static Typeface::Ptr from (const Font& font) { if (auto* cache = MemoryFontCache::getInstance()) + { if (auto result = cache->find ({ font.getTypefaceName(), font.getTypefaceStyle() })) - return new AndroidTypeface (DoCache::no, result->font, result->nonPortableMetrics, font.getTypefaceName(), font.getTypefaceStyle()); + { + return new AndroidTypeface (DoCache::no, + result->font, + result->nonPortableMetrics, + font.getTypefaceName(), + font.getTypefaceStyle(), + result->javaFont); + } + } auto [blob, metrics] = getBlobForFont (font); auto face = FontStyleHelpers::getFaceForBlob ({ static_cast (blob.getData()), blob.getSize() }, 0); @@ -253,7 +266,16 @@ public: HbFont hbFont { hb_font_create (face.get()) }; FontStyleHelpers::initSynthetics (hbFont.get(), font); - return new AndroidTypeface (DoCache::no, std::move (hbFont), metrics, font.getTypefaceName(), font.getTypefaceStyle()); + const auto androidFont = shouldStoreAndroidFont (face.get()) + ? makeAndroidFont ({ static_cast (blob.getData()), blob.getSize() }, 0) + : GlobalRef{}; + + return new AndroidTypeface (DoCache::no, + std::move (hbFont), + metrics, + font.getTypefaceName(), + font.getTypefaceStyle(), + androidFont); } static Typeface::Ptr from (Span blob, unsigned int index = 0) @@ -263,7 +285,7 @@ public: Native getNativeDetails() const override { - return Native { hbFont.get(), nonPortableMetrics }; + return Native { hbFont.get(), nonPortableMetrics, this }; } Typeface::Ptr createSystemFallback (const String& text, const String& language) const override @@ -292,6 +314,11 @@ public: } private: + enum class DoCache + { + no, + yes + }; // The definition of __BIONIC_AVAILABILITY was changed in NDK 28.1 and it now has variadic // parameters. @@ -366,6 +393,40 @@ private: JUCE_END_IGNORE_WARNINGS_GCC_LIKE + static bool shouldStoreAndroidFont (hb_face_t* face) + { + return (hb_ot_color_has_svg (face) || hb_ot_color_has_paint (face)) + && ! (hb_ot_color_has_layers (face) || hb_ot_color_has_png (face)); + } + + static GlobalRef makeAndroidFont (Span blob, unsigned int index) + { + auto* env = getEnv(); + + LocalRef bytes { env->NewByteArray ((jint) blob.size()) }; + { + auto* elements = env->GetByteArrayElements (bytes, nullptr); + const ScopeGuard scope { [&] { env->ReleaseByteArrayElements (bytes, elements, 0); }}; + std::transform (blob.begin(), blob.end(), elements, [] (auto x) { return (jbyte) x; }); + } + + LocalRef byteBuffer { env->CallStaticObjectMethod (JavaByteBuffer, + JavaByteBuffer.allocateDirect, + (jint) blob.size()) }; + env->CallObjectMethod (byteBuffer, JavaByteBuffer.put, bytes.get()); + + LocalRef builder { env->NewObject (AndroidFontBuilder, + AndroidFontBuilder.create, + byteBuffer.get()) }; + env->CallObjectMethod (builder, + AndroidFontBuilder.setTtcIndex, + (jint) index); + LocalRef androidFont { env->CallObjectMethod (builder, + AndroidFontBuilder.build) }; + + return GlobalRef { androidFont }; + } + static Typeface::Ptr loadCompatibleFont (const TypefaceFileAndIndex& info) { FileInputStream stream { info.file }; @@ -376,26 +437,18 @@ private: MemoryBlock mb; stream.readIntoMemoryBlock (mb); - auto result = fromMemory (DoCache::no, - { static_cast (mb.getData()), mb.getSize() }, - (unsigned int) info.index); - - if (result == nullptr) - return {}; - - const auto tech = result->getColourGlyphFormats(); - const auto hasSupportedColours = (tech & (colourGlyphFormatCOLRv0 | colourGlyphFormatBitmap)) != 0; - - // If the font only uses unsupported colour technologies, assume it's the system emoji font - // and try to return a compatible version of the font - if (tech != 0 && ! hasSupportedColours) - if (auto fallback = from (FontOptions { "NotoColorEmojiLegacy", FontValues::defaultFontHeight, Font::plain }); fallback != nullptr) - return fallback; - - return result; + return fromMemory (DoCache::no, + { static_cast (mb.getData()), mb.getSize() }, + (unsigned int) info.index); } - static Typeface::Ptr fromMemory (DoCache cache, Span blob, unsigned int index = 0) + /* The originalSource arg allows the font data to be read again if necessary, perhaps to create a + Java Font instance. Pass a default-constructed File if the font data isn't backed by a + persistent file. + */ + static Typeface::Ptr fromMemory (DoCache cache, + Span blob, + unsigned int index = 0) { auto face = FontStyleHelpers::getFaceForBlob ({ reinterpret_cast (blob.data()), blob.size() }, index); @@ -408,7 +461,8 @@ private: HbFont { hb_font_create (face.get()) }, metrics, readFontName (face.get(), HB_OT_NAME_ID_FONT_FAMILY, nullptr), - readFontName (face.get(), HB_OT_NAME_ID_FONT_SUBFAMILY, nullptr)); + readFontName (face.get(), HB_OT_NAME_ID_FONT_SUBFAMILY, nullptr), + shouldStoreAndroidFont (face.get()) ? makeAndroidFont (blob, index) : GlobalRef{}); } static String readFontName (hb_face_t* face, hb_ot_name_id_t nameId, hb_language_t language) @@ -426,15 +480,17 @@ private: std::shared_ptr fontIn, TypefaceAscentDescent nonPortableMetricsIn, const String& name, - const String& style) + const String& style, + GlobalRef javaFontIn) : Typeface (name, style), hbFont (std::move (fontIn)), doCache (cache), - nonPortableMetrics (nonPortableMetricsIn) + nonPortableMetrics (nonPortableMetricsIn), + javaFont (std::move (javaFontIn)) { if (doCache == DoCache::yes) if (auto* c = MemoryFontCache::getInstance()) - c->add ({ name, style }, { hbFont, nonPortableMetrics }); + c->add ({ name, style }, { hbFont, javaFont, nonPortableMetrics }); } static std::tuple getBlobForFont (const Font& font) @@ -615,9 +671,115 @@ private: fullDescent / referenceFontSize }; } + std::vector getFallbackColourGlyphLayers (int glyph, + const AffineTransform& transform) const override + { + // Canvas.drawGlyphs is only available from API 31 + if (getAndroidSDKVersion() < 31) + return {}; + + auto* env = getEnv(); + + hb_glyph_extents_t extents{}; + + if (! hb_font_get_glyph_extents (hbFont.get(), (hb_codepoint_t) glyph, &extents)) + { + // Trying to retrieve an image for a glyph that's not present in the font? + jassertfalse; + return {}; + } + + const auto upem = (jint) hb_face_get_upem (hb_font_get_face (hbFont.get())); + constexpr jint referenceSize = 128; + + const jint pixelW = (referenceSize * abs (extents.width)) / upem; + const jint pixelH = (referenceSize * abs (extents.height)) / upem; + const jint pixelBearingX = (referenceSize * extents.x_bearing) / upem; + const jint pixelBearingY = (referenceSize * extents.y_bearing) / upem; + + const jint pixelPadding = 2; + + const auto totalW = (size_t) (pixelW + pixelPadding * 2); + const auto totalH = (size_t) (pixelH + pixelPadding * 2); + + LocalRef bitmapConfig { env->CallStaticObjectMethod (AndroidBitmapConfig, + AndroidBitmapConfig.valueOf, + javaString ("ARGB_8888").get()) }; + + LocalRef bitmap { env->CallStaticObjectMethod (AndroidBitmap, + AndroidBitmap.createBitmap, + totalW, + totalH, + bitmapConfig.get()) }; + + LocalRef canvas { env->NewObject (AndroidCanvas, AndroidCanvas.create, bitmap.get())}; + + const jint glyphIdsIn[] { glyph }; + LocalRef glyphIds { env->NewIntArray (std::size (glyphIdsIn)) }; + env->SetIntArrayRegion (glyphIds, 0, std::size (glyphIdsIn), glyphIdsIn); + + const jfloat pos[] { (float) (pixelPadding - pixelBearingX), + (float) (pixelPadding + pixelBearingY) }; + LocalRef positions { env->NewFloatArray (std::size (pos)) }; + env->SetFloatArrayRegion (positions, 0, std::size (pos), pos); + + LocalRef paint { env->NewObject (AndroidPaint, AndroidPaint.defaultConstructor) }; + env->CallVoidMethod (paint, AndroidPaint.setTextSize, (jfloat) referenceSize); + + env->CallVoidMethod (canvas, + AndroidCanvas31.drawGlyphs, + glyphIds.get(), + 0, + positions.get(), + 0, + (jint) std::size (glyphIdsIn), + javaFont.get(), + paint.get()); + + LocalRef pixels { env->NewIntArray ((jint) totalW * (jint) totalH) }; + env->CallVoidMethod (bitmap, + AndroidBitmap.getPixels, + pixels.get(), + 0, + totalW, + 0, + 0, + totalW, + totalH); + + auto* colours = env->GetIntArrayElements (pixels, nullptr); + + ScopeGuard scope { [&] { env->ReleaseIntArrayElements (pixels, colours, JNI_ABORT); } }; + + Image resultImage { Image::ARGB, (int) totalW, (int) totalH, false }; + + // This image will be upside-down, but we'll use the final transform to flip it + { + Image::BitmapData bitmapData { resultImage, Image::BitmapData::writeOnly }; + + for (size_t y = 0; y < totalH; ++y) + { + for (size_t x = 0; x < totalW; ++x) + { + bitmapData.setPixelColour ((int) x, + (int) y, + Colour ((uint32) colours[x + y * totalW])); + } + } + } + + const auto scaleFactor = (float) upem / (float) referenceSize; + return { GlyphLayer { ImageLayer { resultImage, + AffineTransform::translation ((float) pixelBearingX, + (float) -pixelBearingY) + .scaled (scaleFactor, -scaleFactor) + .followedBy (transform) } } }; + } + std::shared_ptr hbFont; DoCache doCache; TypefaceAscentDescent nonPortableMetrics; + GlobalRef javaFont; }; //============================================================================== diff --git a/modules/juce_graphics/native/juce_RenderingHelpers.h b/modules/juce_graphics/native/juce_RenderingHelpers.h index 69bc0f36cf..c6436a4a9d 100644 --- a/modules/juce_graphics/native/juce_RenderingHelpers.h +++ b/modules/juce_graphics/native/juce_RenderingHelpers.h @@ -210,8 +210,7 @@ public: return typeface->getLayersForGlyph (key.font.getMetricsKind(), key.glyph, AffineTransform::scale (fontHeight * key.font.getHorizontalScale(), - fontHeight), - fontHeight); + fontHeight)); }); } @@ -2668,7 +2667,7 @@ protected: const auto fontTransform = AffineTransform::scale (fontHeight * stack->font.getHorizontalScale(), fontHeight).followedBy (t); const auto fullTransform = stack->transform.getTransformWith (fontTransform); - return std::tuple (stack->font.getTypefacePtr()->getLayersForGlyph (stack->font.getMetricsKind(), i, fullTransform, fontHeight), Point{}); + return std::tuple (stack->font.getTypefacePtr()->getLayersForGlyph (stack->font.getMetricsKind(), i, fullTransform), Point{}); }(); const auto initialFill = stack->fillType;