1
0
Fork 0
mirror of https://github.com/juce-framework/JUCE.git synced 2026-01-10 23:44:24 +00:00

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.
This commit is contained in:
reuk 2025-06-09 17:06:52 +01:00
parent 70a2dd7e15
commit a3d64c7784
No known key found for this signature in database
5 changed files with 243 additions and 46 deletions

View file

@ -540,6 +540,7 @@ DECLARE_JNI_CLASS (AndroidPackageManager, "android/content/pm/PackageManager")
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
METHOD (constructor, "<init>", "(I)V") \
METHOD (defaultConstructor, "<init>", "()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

View file

@ -175,11 +175,21 @@ using HbFace = std::unique_ptr<hb_face_t, FunctionPointerDestructor<hb_face_de
using HbBuffer = std::unique_ptr<hb_buffer_t, FunctionPointerDestructor<hb_buffer_destroy>>;
using HbBlob = std::unique_ptr<hb_blob_t, FunctionPointerDestructor<hb_blob_destroy>>;
struct TypefaceFallbackColourGlyphSupport
{
virtual ~TypefaceFallbackColourGlyphSupport() = default;
virtual std::vector<GlyphLayer> 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<GlyphLayer> 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<GlyphLayer> getBitmapLayer (const Typeface& typeface, int gly
return { GlyphLayer { ImageLayer { juceImage, transform } } };
}
std::vector<GlyphLayer> Typeface::getLayersForGlyph (TypefaceMetricsKind kind, int glyphNumber, const AffineTransform& transform, float) const
std::vector<GlyphLayer> 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<GlyphLayer> 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())

View file

@ -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<GlyphLayer> 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.

View file

@ -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, "<init>", "(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, "<init>", "()V") \
METHOD (computeBounds, "computeBounds", "(Landroid/graphics/RectF;Z)V")
@ -98,6 +106,7 @@ std::unique_ptr<InputStream> makeAndroidInputStreamWrapper (LocalRef<jobject> st
struct AndroidCachedTypeface
{
std::shared_ptr<hb_font_t> 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<const char*> (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<const std::byte*> (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<const std::byte> 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<const std::byte> blob, unsigned int index)
{
auto* env = getEnv();
LocalRef<jbyteArray> 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<jobject> byteBuffer { env->CallStaticObjectMethod (JavaByteBuffer,
JavaByteBuffer.allocateDirect,
(jint) blob.size()) };
env->CallObjectMethod (byteBuffer, JavaByteBuffer.put, bytes.get());
LocalRef<jobject> builder { env->NewObject (AndroidFontBuilder,
AndroidFontBuilder.create,
byteBuffer.get()) };
env->CallObjectMethod (builder,
AndroidFontBuilder.setTtcIndex,
(jint) index);
LocalRef<jobject> 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<const std::byte*> (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<const std::byte*> (mb.getData()), mb.getSize() },
(unsigned int) info.index);
}
static Typeface::Ptr fromMemory (DoCache cache, Span<const std::byte> 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<const std::byte> blob,
unsigned int index = 0)
{
auto face = FontStyleHelpers::getFaceForBlob ({ reinterpret_cast<const char*> (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<hb_font_t> 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<MemoryBlock, TypefaceAscentDescent> getBlobForFont (const Font& font)
@ -615,9 +671,115 @@ private:
fullDescent / referenceFontSize };
}
std::vector<GlyphLayer> 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<jobject> bitmapConfig { env->CallStaticObjectMethod (AndroidBitmapConfig,
AndroidBitmapConfig.valueOf,
javaString ("ARGB_8888").get()) };
LocalRef<jobject> bitmap { env->CallStaticObjectMethod (AndroidBitmap,
AndroidBitmap.createBitmap,
totalW,
totalH,
bitmapConfig.get()) };
LocalRef<jobject> canvas { env->NewObject (AndroidCanvas, AndroidCanvas.create, bitmap.get())};
const jint glyphIdsIn[] { glyph };
LocalRef<jintArray> glyphIds { env->NewIntArray (std::size (glyphIdsIn)) };
env->SetIntArrayRegion (glyphIds, 0, std::size (glyphIdsIn), glyphIdsIn);
const jfloat pos[] { (float) (pixelPadding - pixelBearingX),
(float) (pixelPadding + pixelBearingY) };
LocalRef<jfloatArray> positions { env->NewFloatArray (std::size (pos)) };
env->SetFloatArrayRegion (positions, 0, std::size (pos), pos);
LocalRef<jobject> 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<jintArray> 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<hb_font_t> hbFont;
DoCache doCache;
TypefaceAscentDescent nonPortableMetrics;
GlobalRef javaFont;
};
//==============================================================================

View file

@ -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<float>{});
return std::tuple (stack->font.getTypefacePtr()->getLayersForGlyph (stack->font.getMetricsKind(), i, fullTransform), Point<float>{});
}();
const auto initialFill = stack->fillType;