|  | /*
  ==============================================================================
   This file is part of the JUCE 6 technical preview.
   Copyright (c) 2020 - Raw Material Software Limited
   You may use this code under the terms of the GPL v3
   (see www.gnu.org/licenses).
   For this technical preview, this file is not subject to commercial licensing.
   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,          "<init>",           "()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,   "<init>",   "()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<jobject>(env->CallStaticObjectMethod (TypefaceClass, TypefaceClass.createFromFile,
                                                                                     javaString (fontFile.getFullPathName()).get())));
            else
                typeface = GlobalRef (LocalRef<jobject>(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<uint64> (reinterpret_cast<uintptr_t> (data))), String())
    {
        auto* env = getEnv();
        auto cacheFile = getCacheFileForData (data, size);
        typeface = GlobalRef (LocalRef<jobject>(env->CallStaticObjectMethod (TypefaceClass, TypefaceClass.createFromFile,
                                                                             javaString (cacheFile.getFullPathName()).get())));
        initialise (env);
    }
    void initialise (JNIEnv* const env)
    {
        rect = GlobalRef (LocalRef<jobject>(env->NewObject (AndroidRect, AndroidRect.constructor, 0, 0, 0, 0)));
        paint = GlobalRef (GraphicsHelpers::createPaint (Graphics::highResamplingQuality));
        const LocalRef<jobject> ignored (paint.callObjectMethod (AndroidPaint.setTypeface, typeface.get()));
        charArray = GlobalRef (LocalRef<jobject>((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<jfloat> localWidths (static_cast<size_t> (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<int>& glyphs, Array<float>& 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<jfloat> localWidths (static_cast<size_t> (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<jchar>::max())
                {
                    const unsigned int previousGlyph
                        = static_cast<unsigned int> (glyphNumber) & ((1U << (sizeof (jchar) * 8U)) - 1U);
                    const unsigned int thisGlyph
                        = static_cast<unsigned int> (ch)          & ((1U << (sizeof (jchar) * 8U)) - 1U);
                    glyphNumber = static_cast<int> ((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<unsigned int> (glyphNumber) >> 0)                     & ((1U << (sizeof (jchar) * 8U)) - 1U);
        jchar ch2 = (static_cast<unsigned int> (glyphNumber) >> (sizeof (jchar) * 8U)) & ((1U << (sizeof (jchar) * 8U)) - 1U);
       #else
        jchar ch1 = glyphNumber, ch2 = 0;
       #endif
        Rectangle<int> bounds;
        auto* env = getEnv();
        {
            LocalRef<jobject> 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<jobject> path (env->NewObject (AndroidPath, AndroidPath.constructor));
            LocalRef<jobject> 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<int>::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<jobject> bitmapConfig (env->CallStaticObjectMethod (AndroidBitmapConfig, AndroidBitmapConfig.valueOf, javaString ("ARGB_8888").get()));
            LocalRef<jobject> bitmap (env->CallStaticObjectMethod (AndroidBitmap, AndroidBitmap.createBitmap, w, h, bitmapConfig.get()));
            LocalRef<jobject> canvas (env->NewObject (AndroidCanvas, AndroidCanvas.create, bitmap.get()));
            env->CallBooleanMethod (matrix.get(), AndroidMatrix.postTranslate, bounds.getX() * -1.0f, bounds.getY() * -1.0f);
            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> ((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<jobject> getTypefaceFromAsset (const String& typefaceName)
    {
        auto* env = getEnv();
        LocalRef<jobject> assetManager (env->CallObjectMethod (getAppContext().get(), AndroidContext.getAssets));
        if (assetManager == nullptr)
            return LocalRef<jobject>();
        auto assetTypeface = env->CallStaticObjectMethod (TypefaceClass, TypefaceClass.createFromAsset, assetManager.get(),
                                                          javaString ("fonts/" + typefaceName).get());
        // this may throw
        if (env->ExceptionCheck() != 0)
        {
            env->ExceptionClear();
            return LocalRef<jobject>();
        }
        return LocalRef<jobject> (assetTypeface);
    }
    static File getCacheDirectory()
    {
        static File result = []()
        {
            auto appContext = getAppContext();
            if (appContext != nullptr)
            {
                auto* env = getEnv();
                LocalRef<jobject> cacheFile (env->CallObjectMethod (appContext.get(), AndroidContext.getCacheDir));
                LocalRef<jstring> jPath ((jstring) env->CallObjectMethod (cacheFile.get(), JavaFile.getAbsolutePath));
                return File (juceString (env, jPath.get()));
            }
            jassertfalse;
            return File();
        } ();
        return result;
    }
    static HashMap<String, File>& getInMemoryFontCache()
    {
        static HashMap<String, File> cache;
        return cache;
    }
    static File getCacheFileForData (const void* data, size_t size)
    {
        static CriticalSection cs;
        JNIEnv* const env = getEnv();
        String key;
        {
            LocalRef<jobject> digest (env->CallStaticObjectMethod (JavaMessageDigest, JavaMessageDigest.getInstance, javaString("MD5").get()));
            LocalRef<jbyteArray> 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<jbyteArray> 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
 |