/* ============================================================================== This file is part of the JUCE library. Copyright (c) 2022 - Raw Material Software Limited JUCE is an open source library subject to commercial or open-source licensing. By using JUCE, you agree to the terms of both the JUCE 7 End-User License Agreement and JUCE Privacy Policy. End User License Agreement: www.juce.com/juce-7-licence Privacy Policy: www.juce.com/juce-privacy-policy Or: You may also use this code under the terms of the GPL v3 (see www.gnu.org/licenses). JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE DISCLAIMED. ============================================================================== */ namespace juce { struct DefaultFontNames { DefaultFontNames() : defaultSans ("sans"), defaultSerif ("serif"), defaultFixed ("monospace"), defaultFallback ("sans") { } String getRealFontName (const String& faceName) const { if (faceName == Font::getDefaultSansSerifFontName()) return defaultSans; if (faceName == Font::getDefaultSerifFontName()) return defaultSerif; if (faceName == Font::getDefaultMonospacedFontName()) return defaultFixed; return faceName; } String defaultSans, defaultSerif, defaultFixed, defaultFallback; }; Typeface::Ptr Font::getDefaultTypefaceForFont (const Font& font) { static DefaultFontNames defaultNames; Font f (font); f.setTypefaceName (defaultNames.getRealFontName (font.getTypefaceName())); return Typeface::createSystemTypefaceFor (f); } //============================================================================== #if JUCE_USE_FREETYPE StringArray FTTypefaceList::getDefaultFontDirectories() { return StringArray ("/system/fonts"); } Typeface::Ptr Typeface::createSystemTypefaceFor (const Font& font) { return new FreeTypeTypeface (font); } void Typeface::scanFolderForFonts (const File& folder) { FTTypefaceList::getInstance()->scanFontPaths (StringArray (folder.getFullPathName())); } StringArray Font::findAllTypefaceNames() { return FTTypefaceList::getInstance()->findAllFamilyNames(); } StringArray Font::findAllTypefaceStyles (const String& family) { return FTTypefaceList::getInstance()->findAllTypefaceStyles (family); } bool TextLayout::createNativeLayout (const AttributedString&) { return false; } #else //============================================================================== #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ STATICMETHOD (create, "create", "(Ljava/lang/String;I)Landroid/graphics/Typeface;") \ STATICMETHOD (createFromFile, "createFromFile", "(Ljava/lang/String;)Landroid/graphics/Typeface;") \ STATICMETHOD (createFromAsset, "createFromAsset", "(Landroid/content/res/AssetManager;Ljava/lang/String;)Landroid/graphics/Typeface;") DECLARE_JNI_CLASS (TypefaceClass, "android/graphics/Typeface") #undef JNI_CLASS_MEMBERS #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ METHOD (constructor, "", "()V") \ METHOD (computeBounds, "computeBounds", "(Landroid/graphics/RectF;Z)V") DECLARE_JNI_CLASS (AndroidPath, "android/graphics/Path") #undef JNI_CLASS_MEMBERS #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ METHOD (constructor, "", "()V") \ FIELD (left, "left", "F") \ FIELD (right, "right", "F") \ FIELD (top, "top", "F") \ FIELD (bottom, "bottom", "F") \ METHOD (roundOut, "roundOut", "(Landroid/graphics/Rect;)V") DECLARE_JNI_CLASS (AndroidRectF, "android/graphics/RectF") #undef JNI_CLASS_MEMBERS #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ STATICMETHOD (getInstance, "getInstance", "(Ljava/lang/String;)Ljava/security/MessageDigest;") \ METHOD (update, "update", "([B)V") \ METHOD (digest, "digest", "()[B") DECLARE_JNI_CLASS (JavaMessageDigest, "java/security/MessageDigest") #undef JNI_CLASS_MEMBERS //============================================================================== StringArray Font::findAllTypefaceNames() { StringArray results; for (auto& f : File ("/system/fonts").findChildFiles (File::findFiles, false, "*.ttf")) results.addIfNotAlreadyThere (f.getFileNameWithoutExtension().upToLastOccurrenceOf ("-", false, false)); return results; } StringArray Font::findAllTypefaceStyles (const String& family) { StringArray results ("Regular"); for (auto& f : File ("/system/fonts").findChildFiles (File::findFiles, false, family + "-*.ttf")) results.addIfNotAlreadyThere (f.getFileNameWithoutExtension().fromLastOccurrenceOf ("-", false, false)); return results; } const float referenceFontSize = 256.0f; const float referenceFontToUnits = 1.0f / referenceFontSize; //============================================================================== class AndroidTypeface : public Typeface { public: AndroidTypeface (const Font& font) : Typeface (font.getTypefaceName(), font.getTypefaceStyle()), ascent (0), descent (0), heightToPointsFactor (1.0f) { JNIEnv* const env = getEnv(); // First check whether there's an embedded asset with this font name: typeface = GlobalRef (getTypefaceFromAsset (name)); if (typeface.get() == nullptr) { const bool isBold = style.contains ("Bold"); const bool isItalic = style.contains ("Italic"); File fontFile (getFontFile (name, style)); if (! fontFile.exists()) fontFile = findFontFile (name, isBold, isItalic); if (fontFile.exists()) typeface = GlobalRef (LocalRef(env->CallStaticObjectMethod (TypefaceClass, TypefaceClass.createFromFile, javaString (fontFile.getFullPathName()).get()))); else typeface = GlobalRef (LocalRef(env->CallStaticObjectMethod (TypefaceClass, TypefaceClass.create, javaString (getName()).get(), (isBold ? 1 : 0) + (isItalic ? 2 : 0)))); } initialise (env); } AndroidTypeface (const void* data, size_t size) : Typeface (String (static_cast (reinterpret_cast (data))), String()) { auto* env = getEnv(); auto cacheFile = getCacheFileForData (data, size); typeface = GlobalRef (LocalRef(env->CallStaticObjectMethod (TypefaceClass, TypefaceClass.createFromFile, javaString (cacheFile.getFullPathName()).get()))); initialise (env); } void initialise (JNIEnv* const env) { rect = GlobalRef (LocalRef(env->NewObject (AndroidRect, AndroidRect.constructor, 0, 0, 0, 0))); paint = GlobalRef (GraphicsHelpers::createPaint (Graphics::highResamplingQuality)); const LocalRef ignored (paint.callObjectMethod (AndroidPaint.setTypeface, typeface.get())); charArray = GlobalRef (LocalRef((jobject) env->NewCharArray (2))); paint.callVoidMethod (AndroidPaint.setTextSize, referenceFontSize); const float fullAscent = std::abs (paint.callFloatMethod (AndroidPaint.ascent)); const float fullDescent = paint.callFloatMethod (AndroidPaint.descent); const float totalHeight = fullAscent + fullDescent; ascent = fullAscent / totalHeight; descent = fullDescent / totalHeight; heightToPointsFactor = referenceFontSize / totalHeight; } float getAscent() const override { return ascent; } float getDescent() const override { return descent; } float getHeightToPointsFactor() const override { return heightToPointsFactor; } float getStringWidth (const String& text) override { JNIEnv* env = getEnv(); auto numChars = CharPointer_UTF16::getBytesRequiredFor (text.getCharPointer()); jfloatArray widths = env->NewFloatArray ((int) numChars); const int numDone = paint.callIntMethod (AndroidPaint.getTextWidths, javaString (text).get(), widths); HeapBlock localWidths (static_cast (numDone)); env->GetFloatArrayRegion (widths, 0, numDone, localWidths); env->DeleteLocalRef (widths); float x = 0; for (int i = 0; i < numDone; ++i) x += localWidths[i]; return x * referenceFontToUnits; } void getGlyphPositions (const String& text, Array& glyphs, Array& xOffsets) override { JNIEnv* env = getEnv(); auto numChars = CharPointer_UTF16::getBytesRequiredFor (text.getCharPointer()); jfloatArray widths = env->NewFloatArray ((int) numChars); const int numDone = paint.callIntMethod (AndroidPaint.getTextWidths, javaString (text).get(), widths); HeapBlock localWidths (static_cast (numDone)); env->GetFloatArrayRegion (widths, 0, numDone, localWidths); env->DeleteLocalRef (widths); auto s = text.getCharPointer(); xOffsets.add (0); float x = 0; for (int i = 0; i < numDone; ++i) { const float local = localWidths[i]; // Android uses jchar (UTF-16) characters jchar ch = (jchar) s.getAndAdvance(); // Android has no proper glyph support, so we have to do // a hacky workaround for ligature detection #if JUCE_STRING_UTF_TYPE <= 16 static_assert (sizeof (int) >= (sizeof (jchar) * 2), "Unable store two java chars in one glyph"); // if the width of this glyph is zero inside the string but has // a width on it's own, then it's probably due to ligature if (local == 0.0f && glyphs.size() > 0 && getStringWidth (String (ch)) > 0.0f) { // modify the previous glyph int& glyphNumber = glyphs.getReference (glyphs.size() - 1); // make sure this is not a three character ligature if (glyphNumber < std::numeric_limits::max()) { const unsigned int previousGlyph = static_cast (glyphNumber) & ((1U << (sizeof (jchar) * 8U)) - 1U); const unsigned int thisGlyph = static_cast (ch) & ((1U << (sizeof (jchar) * 8U)) - 1U); glyphNumber = static_cast ((thisGlyph << (sizeof (jchar) * 8U)) | previousGlyph); ch = 0; } } #endif glyphs.add ((int) ch); x += local; xOffsets.add (x * referenceFontToUnits); } } bool getOutlineForGlyph (int /*glyphNumber*/, Path& /*destPath*/) override { return false; } EdgeTable* getEdgeTableForGlyph (int glyphNumber, const AffineTransform& t, float /*fontHeight*/) override { #if JUCE_STRING_UTF_TYPE <= 16 static_assert (sizeof (int) >= (sizeof (jchar) * 2), "Unable store two jni chars in one int"); // glyphNumber of zero is used to indicate that the last character was a ligature if (glyphNumber == 0) return nullptr; jchar ch1 = (static_cast (glyphNumber) >> 0) & ((1U << (sizeof (jchar) * 8U)) - 1U); jchar ch2 = (static_cast (glyphNumber) >> (sizeof (jchar) * 8U)) & ((1U << (sizeof (jchar) * 8U)) - 1U); #else jchar ch1 = glyphNumber, ch2 = 0; #endif Rectangle bounds; auto* env = getEnv(); { LocalRef matrix (GraphicsHelpers::createMatrix (env, AffineTransform::scale (referenceFontToUnits).followedBy (t))); jboolean isCopy; auto* buffer = env->GetCharArrayElements ((jcharArray) charArray.get(), &isCopy); buffer[0] = ch1; buffer[1] = ch2; env->ReleaseCharArrayElements ((jcharArray) charArray.get(), buffer, 0); LocalRef path (env->NewObject (AndroidPath, AndroidPath.constructor)); LocalRef boundsF (env->NewObject (AndroidRectF, AndroidRectF.constructor)); env->CallVoidMethod (paint.get(), AndroidPaint.getCharsPath, charArray.get(), 0, (ch2 != 0 ? 2 : 1), 0.0f, 0.0f, path.get()); env->CallVoidMethod (path.get(), AndroidPath.computeBounds, boundsF.get(), 1); env->CallBooleanMethod (matrix.get(), AndroidMatrix.mapRect, boundsF.get()); env->CallVoidMethod (boundsF.get(), AndroidRectF.roundOut, rect.get()); bounds = Rectangle::leftTopRightBottom (env->GetIntField (rect.get(), AndroidRect.left) - 1, env->GetIntField (rect.get(), AndroidRect.top), env->GetIntField (rect.get(), AndroidRect.right) + 1, env->GetIntField (rect.get(), AndroidRect.bottom)); auto w = bounds.getWidth(); auto h = jmax (1, bounds.getHeight()); LocalRef bitmapConfig (env->CallStaticObjectMethod (AndroidBitmapConfig, AndroidBitmapConfig.valueOf, javaString ("ARGB_8888").get())); LocalRef bitmap (env->CallStaticObjectMethod (AndroidBitmap, AndroidBitmap.createBitmap, w, h, bitmapConfig.get())); LocalRef canvas (env->NewObject (AndroidCanvas, AndroidCanvas.create, bitmap.get())); env->CallBooleanMethod (matrix.get(), AndroidMatrix.postTranslate, (float) -bounds.getX(), (float) -bounds.getY()); env->CallVoidMethod (canvas.get(), AndroidCanvas.setMatrix, matrix.get()); env->CallVoidMethod (canvas.get(), AndroidCanvas.drawPath, path.get(), paint.get()); int requiredRenderArraySize = w * h; if (requiredRenderArraySize > lastCachedRenderArraySize) { cachedRenderArray = GlobalRef (LocalRef ((jobject) env->NewIntArray (requiredRenderArraySize))); lastCachedRenderArraySize = requiredRenderArraySize; } env->CallVoidMethod (bitmap.get(), AndroidBitmap.getPixels, cachedRenderArray.get(), 0, w, 0, 0, w, h); env->CallVoidMethod (bitmap.get(), AndroidBitmap.recycle); } EdgeTable* et = nullptr; if (! bounds.isEmpty()) { et = new EdgeTable (bounds); jint* const maskDataElements = env->GetIntArrayElements ((jintArray) cachedRenderArray.get(), nullptr); const jint* mask = maskDataElements; for (int y = bounds.getY(); y < bounds.getBottom(); ++y) { #if JUCE_LITTLE_ENDIAN const uint8* const lineBytes = ((const uint8*) mask) + 3; #else const uint8* const lineBytes = (const uint8*) mask; #endif et->clipLineToMask (bounds.getX(), y, lineBytes, 4, bounds.getWidth()); mask += bounds.getWidth(); } env->ReleaseIntArrayElements ((jintArray) cachedRenderArray.get(), maskDataElements, 0); } return et; } GlobalRef typeface, paint, rect, charArray, cachedRenderArray; float ascent, descent, heightToPointsFactor; int lastCachedRenderArraySize = -1; private: static File findFontFile (const String& family, const bool bold, const bool italic) { File file; if (bold || italic) { String suffix; if (bold) suffix = "Bold"; if (italic) suffix << "Italic"; file = getFontFile (family, suffix); if (file.exists()) return file; } file = getFontFile (family, "Regular"); if (! file.exists()) file = getFontFile (family, String()); return file; } static File getFontFile (const String& family, const String& fontStyle) { String path ("/system/fonts/" + family); if (fontStyle.isNotEmpty()) path << '-' << fontStyle; return File (path + ".ttf"); } static LocalRef getTypefaceFromAsset (const String& typefaceName) { auto* env = getEnv(); LocalRef assetManager (env->CallObjectMethod (getAppContext().get(), AndroidContext.getAssets)); if (assetManager == nullptr) return LocalRef(); auto assetTypeface = env->CallStaticObjectMethod (TypefaceClass, TypefaceClass.createFromAsset, assetManager.get(), javaString ("fonts/" + typefaceName).get()); // this may throw if (env->ExceptionCheck() != 0) { env->ExceptionClear(); return LocalRef(); } return LocalRef (assetTypeface); } static File getCacheDirectory() { static File result = []() { auto appContext = getAppContext(); if (appContext != nullptr) { auto* env = getEnv(); LocalRef cacheFile (env->CallObjectMethod (appContext.get(), AndroidContext.getCacheDir)); LocalRef jPath ((jstring) env->CallObjectMethod (cacheFile.get(), JavaFile.getAbsolutePath)); return File (juceString (env, jPath.get())); } jassertfalse; return File(); } (); return result; } static HashMap& getInMemoryFontCache() { static HashMap cache; return cache; } static File getCacheFileForData (const void* data, size_t size) { static CriticalSection cs; JNIEnv* const env = getEnv(); String key; { LocalRef digest (env->CallStaticObjectMethod (JavaMessageDigest, JavaMessageDigest.getInstance, javaString("MD5").get())); LocalRef bytes(env->NewByteArray ((int) size)); jboolean ignore; auto* jbytes = env->GetByteArrayElements(bytes.get(), &ignore); memcpy(jbytes, data, size); env->ReleaseByteArrayElements(bytes.get(), jbytes, 0); env->CallVoidMethod(digest.get(), JavaMessageDigest.update, bytes.get()); LocalRef result((jbyteArray) env->CallObjectMethod(digest.get(), JavaMessageDigest.digest)); auto* md5Bytes = env->GetByteArrayElements(result.get(), &ignore); key = String::toHexString(md5Bytes, env->GetArrayLength(result.get()), 0); env->ReleaseByteArrayElements(result.get(), md5Bytes, 0); } ScopedLock lock (cs); auto& mapEntry = getInMemoryFontCache().getReference (key); if (mapEntry == File()) { mapEntry = getCacheDirectory().getChildFile ("bindata_" + key); mapEntry.replaceWithData (data, size); } return mapEntry; } JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AndroidTypeface) }; //============================================================================== Typeface::Ptr Typeface::createSystemTypefaceFor (const Font& font) { return new AndroidTypeface (font); } Typeface::Ptr Typeface::createSystemTypefaceFor (const void* data, size_t size) { return new AndroidTypeface (data, size); } void Typeface::scanFolderForFonts (const File&) { jassertfalse; // not available unless using FreeType } bool TextLayout::createNativeLayout (const AttributedString&) { return false; } #endif } // namespace juce