1 /*
2   ==============================================================================
3 
4    This file is part of the JUCE library.
5    Copyright (c) 2020 - Raw Material Software Limited
6 
7    JUCE is an open source library subject to commercial or open-source
8    licensing.
9 
10    By using JUCE, you agree to the terms of both the JUCE 6 End-User License
11    Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020).
12 
13    End User License Agreement: www.juce.com/juce-6-licence
14    Privacy Policy: www.juce.com/juce-privacy-policy
15 
16    Or: You may also use this code under the terms of the GPL v3 (see
17    www.gnu.org/licenses).
18 
19    JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
20    EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
21    DISCLAIMED.
22 
23   ==============================================================================
24 */
25 
26 namespace juce
27 {
28 
29 struct DefaultFontNames
30 {
DefaultFontNamesjuce::DefaultFontNames31     DefaultFontNames()
32         : defaultSans  ("sans"),
33           defaultSerif ("serif"),
34           defaultFixed ("monospace"),
35           defaultFallback ("sans")
36     {
37     }
38 
getRealFontNamejuce::DefaultFontNames39     String getRealFontName (const String& faceName) const
40     {
41         if (faceName == Font::getDefaultSansSerifFontName())    return defaultSans;
42         if (faceName == Font::getDefaultSerifFontName())        return defaultSerif;
43         if (faceName == Font::getDefaultMonospacedFontName())   return defaultFixed;
44 
45         return faceName;
46     }
47 
48     String defaultSans, defaultSerif, defaultFixed, defaultFallback;
49 };
50 
getDefaultTypefaceForFont(const Font & font)51 Typeface::Ptr Font::getDefaultTypefaceForFont (const Font& font)
52 {
53     static DefaultFontNames defaultNames;
54 
55     Font f (font);
56     f.setTypefaceName (defaultNames.getRealFontName (font.getTypefaceName()));
57     return Typeface::createSystemTypefaceFor (f);
58 }
59 
60 //==============================================================================
61 #if JUCE_USE_FREETYPE
62 
getDefaultFontDirectories()63 StringArray FTTypefaceList::getDefaultFontDirectories()
64 {
65     return StringArray ("/system/fonts");
66 }
67 
createSystemTypefaceFor(const Font & font)68 Typeface::Ptr Typeface::createSystemTypefaceFor (const Font& font)
69 {
70     return new FreeTypeTypeface (font);
71 }
72 
scanFolderForFonts(const File & folder)73 void Typeface::scanFolderForFonts (const File& folder)
74 {
75     FTTypefaceList::getInstance()->scanFontPaths (StringArray (folder.getFullPathName()));
76 }
77 
findAllTypefaceNames()78 StringArray Font::findAllTypefaceNames()
79 {
80     return FTTypefaceList::getInstance()->findAllFamilyNames();
81 }
82 
findAllTypefaceStyles(const String & family)83 StringArray Font::findAllTypefaceStyles (const String& family)
84 {
85     return FTTypefaceList::getInstance()->findAllTypefaceStyles (family);
86 }
87 
createNativeLayout(const AttributedString &)88 bool TextLayout::createNativeLayout (const AttributedString&)
89 {
90     return false;
91 }
92 
93 #else
94 
95 //==============================================================================
96 #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
97  STATICMETHOD (create,          "create",           "(Ljava/lang/String;I)Landroid/graphics/Typeface;") \
98  STATICMETHOD (createFromFile,  "createFromFile",   "(Ljava/lang/String;)Landroid/graphics/Typeface;") \
99  STATICMETHOD (createFromAsset, "createFromAsset",  "(Landroid/content/res/AssetManager;Ljava/lang/String;)Landroid/graphics/Typeface;")
100 
101  DECLARE_JNI_CLASS (TypefaceClass, "android/graphics/Typeface")
102 #undef JNI_CLASS_MEMBERS
103 
104 #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
105  METHOD (constructor,          "<init>",           "()V") \
106  METHOD (computeBounds,        "computeBounds",     "(Landroid/graphics/RectF;Z)V")
107 
108  DECLARE_JNI_CLASS (AndroidPath, "android/graphics/Path")
109 #undef JNI_CLASS_MEMBERS
110 
111 #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
112  METHOD (constructor,   "<init>",   "()V") \
113  FIELD  (left,           "left",     "F") \
114  FIELD  (right,          "right",    "F") \
115  FIELD  (top,            "top",      "F") \
116  FIELD  (bottom,         "bottom",   "F") \
117  METHOD (roundOut,       "roundOut", "(Landroid/graphics/Rect;)V")
118 
119 DECLARE_JNI_CLASS (AndroidRectF, "android/graphics/RectF")
120 #undef JNI_CLASS_MEMBERS
121 
122 #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
123  STATICMETHOD (getInstance, "getInstance", "(Ljava/lang/String;)Ljava/security/MessageDigest;") \
124  METHOD       (update,      "update",      "([B)V") \
125  METHOD       (digest,      "digest",      "()[B")
126 DECLARE_JNI_CLASS (JavaMessageDigest, "java/security/MessageDigest")
127 #undef JNI_CLASS_MEMBERS
128 
129 //==============================================================================
findAllTypefaceNames()130 StringArray Font::findAllTypefaceNames()
131 {
132     StringArray results;
133 
134     for (auto& f : File ("/system/fonts").findChildFiles (File::findFiles, false, "*.ttf"))
135         results.addIfNotAlreadyThere (f.getFileNameWithoutExtension().upToLastOccurrenceOf ("-", false, false));
136 
137     return results;
138 }
139 
findAllTypefaceStyles(const String & family)140 StringArray Font::findAllTypefaceStyles (const String& family)
141 {
142     StringArray results ("Regular");
143 
144     for (auto& f : File ("/system/fonts").findChildFiles (File::findFiles, false, family + "-*.ttf"))
145         results.addIfNotAlreadyThere (f.getFileNameWithoutExtension().fromLastOccurrenceOf ("-", false, false));
146 
147     return results;
148 }
149 
150 const float referenceFontSize = 256.0f;
151 const float referenceFontToUnits = 1.0f / referenceFontSize;
152 
153 //==============================================================================
154 class AndroidTypeface   : public Typeface
155 {
156 public:
AndroidTypeface(const Font & font)157     AndroidTypeface (const Font& font)
158         : Typeface (font.getTypefaceName(), font.getTypefaceStyle()),
159           ascent (0), descent (0), heightToPointsFactor (1.0f)
160     {
161         JNIEnv* const env = getEnv();
162 
163         // First check whether there's an embedded asset with this font name:
164         typeface = GlobalRef (getTypefaceFromAsset (name));
165 
166         if (typeface.get() == nullptr)
167         {
168             const bool isBold   = style.contains ("Bold");
169             const bool isItalic = style.contains ("Italic");
170 
171             File fontFile (getFontFile (name, style));
172 
173             if (! fontFile.exists())
174                 fontFile = findFontFile (name, isBold, isItalic);
175 
176             if (fontFile.exists())
177                 typeface = GlobalRef (LocalRef<jobject>(env->CallStaticObjectMethod (TypefaceClass, TypefaceClass.createFromFile,
178                                                                                      javaString (fontFile.getFullPathName()).get())));
179             else
180                 typeface = GlobalRef (LocalRef<jobject>(env->CallStaticObjectMethod (TypefaceClass, TypefaceClass.create,
181                                                                                      javaString (getName()).get(),
182                                                                                      (isBold ? 1 : 0) + (isItalic ? 2 : 0))));
183         }
184 
185         initialise (env);
186     }
187 
AndroidTypeface(const void * data,size_t size)188     AndroidTypeface (const void* data, size_t size)
189         : Typeface (String (static_cast<uint64> (reinterpret_cast<uintptr_t> (data))), String())
190     {
191         auto* env = getEnv();
192         auto cacheFile = getCacheFileForData (data, size);
193 
194         typeface = GlobalRef (LocalRef<jobject>(env->CallStaticObjectMethod (TypefaceClass, TypefaceClass.createFromFile,
195                                                                              javaString (cacheFile.getFullPathName()).get())));
196 
197         initialise (env);
198     }
199 
initialise(JNIEnv * const env)200     void initialise (JNIEnv* const env)
201     {
202         rect = GlobalRef (LocalRef<jobject>(env->NewObject (AndroidRect, AndroidRect.constructor, 0, 0, 0, 0)));
203 
204         paint = GlobalRef (GraphicsHelpers::createPaint (Graphics::highResamplingQuality));
205         const LocalRef<jobject> ignored (paint.callObjectMethod (AndroidPaint.setTypeface, typeface.get()));
206 
207         charArray = GlobalRef (LocalRef<jobject>((jobject) env->NewCharArray (2)));
208 
209         paint.callVoidMethod (AndroidPaint.setTextSize, referenceFontSize);
210 
211         const float fullAscent = std::abs (paint.callFloatMethod (AndroidPaint.ascent));
212         const float fullDescent = paint.callFloatMethod (AndroidPaint.descent);
213         const float totalHeight = fullAscent + fullDescent;
214 
215         ascent  = fullAscent / totalHeight;
216         descent = fullDescent / totalHeight;
217         heightToPointsFactor = referenceFontSize / totalHeight;
218     }
219 
getAscent() const220     float getAscent() const override                 { return ascent; }
getDescent() const221     float getDescent() const override                { return descent; }
getHeightToPointsFactor() const222     float getHeightToPointsFactor() const override   { return heightToPointsFactor; }
223 
getStringWidth(const String & text)224     float getStringWidth (const String& text) override
225     {
226         JNIEnv* env = getEnv();
227         auto numChars = CharPointer_UTF16::getBytesRequiredFor (text.getCharPointer());
228         jfloatArray widths = env->NewFloatArray ((int) numChars);
229 
230         const int numDone = paint.callIntMethod (AndroidPaint.getTextWidths, javaString (text).get(), widths);
231 
232         HeapBlock<jfloat> localWidths (static_cast<size_t> (numDone));
233         env->GetFloatArrayRegion (widths, 0, numDone, localWidths);
234         env->DeleteLocalRef (widths);
235 
236         float x = 0;
237 
238         for (int i = 0; i < numDone; ++i)
239             x += localWidths[i];
240 
241         return x * referenceFontToUnits;
242     }
243 
getGlyphPositions(const String & text,Array<int> & glyphs,Array<float> & xOffsets)244     void getGlyphPositions (const String& text, Array<int>& glyphs, Array<float>& xOffsets) override
245     {
246         JNIEnv* env = getEnv();
247         auto numChars = CharPointer_UTF16::getBytesRequiredFor (text.getCharPointer());
248         jfloatArray widths = env->NewFloatArray ((int) numChars);
249 
250         const int numDone = paint.callIntMethod (AndroidPaint.getTextWidths, javaString (text).get(), widths);
251 
252         HeapBlock<jfloat> localWidths (static_cast<size_t> (numDone));
253         env->GetFloatArrayRegion (widths, 0, numDone, localWidths);
254         env->DeleteLocalRef (widths);
255 
256         auto s = text.getCharPointer();
257 
258         xOffsets.add (0);
259         float x = 0;
260 
261         for (int i = 0; i < numDone; ++i)
262         {
263             const float local = localWidths[i];
264 
265             // Android uses jchar (UTF-16) characters
266             jchar ch = (jchar) s.getAndAdvance();
267 
268             // Android has no proper glyph support, so we have to do
269             // a hacky workaround for ligature detection
270 
271            #if JUCE_STRING_UTF_TYPE <= 16
272             static_assert (sizeof (int) >= (sizeof (jchar) * 2), "Unable store two java chars in one glyph");
273 
274             // if the width of this glyph is zero inside the string but has
275             // a width on it's own, then it's probably due to ligature
276             if (local == 0.0f && glyphs.size() > 0 && getStringWidth (String (ch)) > 0.0f)
277             {
278                 // modify the previous glyph
279                 int& glyphNumber = glyphs.getReference (glyphs.size() - 1);
280 
281                 // make sure this is not a three character ligature
282                 if (glyphNumber < std::numeric_limits<jchar>::max())
283                 {
284                     const unsigned int previousGlyph
285                         = static_cast<unsigned int> (glyphNumber) & ((1U << (sizeof (jchar) * 8U)) - 1U);
286                     const unsigned int thisGlyph
287                         = static_cast<unsigned int> (ch)          & ((1U << (sizeof (jchar) * 8U)) - 1U);
288 
289                     glyphNumber = static_cast<int> ((thisGlyph << (sizeof (jchar) * 8U)) | previousGlyph);
290                     ch = 0;
291                 }
292             }
293            #endif
294 
295             glyphs.add ((int) ch);
296             x += local;
297             xOffsets.add (x * referenceFontToUnits);
298         }
299     }
300 
getOutlineForGlyph(int,Path &)301     bool getOutlineForGlyph (int /*glyphNumber*/, Path& /*destPath*/) override
302     {
303         return false;
304     }
305 
getEdgeTableForGlyph(int glyphNumber,const AffineTransform & t,float)306     EdgeTable* getEdgeTableForGlyph (int glyphNumber, const AffineTransform& t, float /*fontHeight*/) override
307     {
308        #if JUCE_STRING_UTF_TYPE <= 16
309         static_assert (sizeof (int) >= (sizeof (jchar) * 2), "Unable store two jni chars in one int");
310 
311         // glyphNumber of zero is used to indicate that the last character was a ligature
312         if (glyphNumber == 0) return nullptr;
313 
314         jchar ch1 = (static_cast<unsigned int> (glyphNumber) >> 0)                     & ((1U << (sizeof (jchar) * 8U)) - 1U);
315         jchar ch2 = (static_cast<unsigned int> (glyphNumber) >> (sizeof (jchar) * 8U)) & ((1U << (sizeof (jchar) * 8U)) - 1U);
316        #else
317         jchar ch1 = glyphNumber, ch2 = 0;
318        #endif
319         Rectangle<int> bounds;
320         auto* env = getEnv();
321 
322         {
323             LocalRef<jobject> matrix (GraphicsHelpers::createMatrix (env, AffineTransform::scale (referenceFontToUnits).followedBy (t)));
324 
325             jboolean isCopy;
326             auto* buffer = env->GetCharArrayElements ((jcharArray) charArray.get(), &isCopy);
327 
328             buffer[0] = ch1; buffer[1] = ch2;
329             env->ReleaseCharArrayElements ((jcharArray) charArray.get(), buffer, 0);
330 
331             LocalRef<jobject> path (env->NewObject (AndroidPath, AndroidPath.constructor));
332             LocalRef<jobject> boundsF (env->NewObject (AndroidRectF, AndroidRectF.constructor));
333 
334 
335             env->CallVoidMethod (paint.get(), AndroidPaint.getCharsPath, charArray.get(), 0, (ch2 != 0 ? 2 : 1), 0.0f, 0.0f, path.get());
336 
337             env->CallVoidMethod (path.get(), AndroidPath.computeBounds, boundsF.get(), 1);
338 
339             env->CallBooleanMethod (matrix.get(), AndroidMatrix.mapRect, boundsF.get());
340 
341             env->CallVoidMethod (boundsF.get(), AndroidRectF.roundOut, rect.get());
342 
343             bounds = Rectangle<int>::leftTopRightBottom (env->GetIntField (rect.get(), AndroidRect.left) - 1,
344                                                          env->GetIntField (rect.get(), AndroidRect.top),
345                                                          env->GetIntField (rect.get(), AndroidRect.right) + 1,
346                                                          env->GetIntField (rect.get(), AndroidRect.bottom));
347 
348             auto w = bounds.getWidth();
349             auto h = jmax (1, bounds.getHeight());
350 
351             LocalRef<jobject> bitmapConfig (env->CallStaticObjectMethod (AndroidBitmapConfig, AndroidBitmapConfig.valueOf, javaString ("ARGB_8888").get()));
352             LocalRef<jobject> bitmap (env->CallStaticObjectMethod (AndroidBitmap, AndroidBitmap.createBitmap, w, h, bitmapConfig.get()));
353             LocalRef<jobject> canvas (env->NewObject (AndroidCanvas, AndroidCanvas.create, bitmap.get()));
354 
355             env->CallBooleanMethod (matrix.get(), AndroidMatrix.postTranslate, bounds.getX() * -1.0f, bounds.getY() * -1.0f);
356             env->CallVoidMethod (canvas.get(), AndroidCanvas.setMatrix, matrix.get());
357             env->CallVoidMethod (canvas.get(), AndroidCanvas.drawPath, path.get(), paint.get());
358 
359             int requiredRenderArraySize = w * h;
360             if (requiredRenderArraySize > lastCachedRenderArraySize)
361             {
362                 cachedRenderArray = GlobalRef (LocalRef<jobject> ((jobject) env->NewIntArray (requiredRenderArraySize)));
363                 lastCachedRenderArraySize = requiredRenderArraySize;
364             }
365 
366             env->CallVoidMethod (bitmap.get(), AndroidBitmap.getPixels, cachedRenderArray.get(), 0, w, 0, 0, w, h);
367             env->CallVoidMethod (bitmap.get(), AndroidBitmap.recycle);
368         }
369 
370         EdgeTable* et = nullptr;
371 
372         if (! bounds.isEmpty())
373         {
374             et = new EdgeTable (bounds);
375 
376             jint* const maskDataElements = env->GetIntArrayElements ((jintArray) cachedRenderArray.get(), nullptr);
377             const jint* mask = maskDataElements;
378 
379             for (int y = bounds.getY(); y < bounds.getBottom(); ++y)
380             {
381                #if JUCE_LITTLE_ENDIAN
382                 const uint8* const lineBytes = ((const uint8*) mask) + 3;
383                #else
384                 const uint8* const lineBytes = (const uint8*) mask;
385                #endif
386 
387                 et->clipLineToMask (bounds.getX(), y, lineBytes, 4, bounds.getWidth());
388                 mask += bounds.getWidth();
389             }
390 
391             env->ReleaseIntArrayElements ((jintArray) cachedRenderArray.get(), maskDataElements, 0);
392         }
393 
394         return et;
395     }
396 
397     GlobalRef typeface, paint, rect, charArray, cachedRenderArray;
398     float ascent, descent, heightToPointsFactor;
399     int lastCachedRenderArraySize = -1;
400 
401 private:
findFontFile(const String & family,const bool bold,const bool italic)402     static File findFontFile (const String& family,
403                               const bool bold, const bool italic)
404     {
405         File file;
406 
407         if (bold || italic)
408         {
409             String suffix;
410             if (bold)   suffix = "Bold";
411             if (italic) suffix << "Italic";
412 
413             file = getFontFile (family, suffix);
414 
415             if (file.exists())
416                 return file;
417         }
418 
419         file = getFontFile (family, "Regular");
420 
421         if (! file.exists())
422             file = getFontFile (family, String());
423 
424         return file;
425     }
426 
getFontFile(const String & family,const String & fontStyle)427     static File getFontFile (const String& family, const String& fontStyle)
428     {
429         String path ("/system/fonts/" + family);
430 
431         if (fontStyle.isNotEmpty())
432             path << '-' << fontStyle;
433 
434         return File (path + ".ttf");
435     }
436 
getTypefaceFromAsset(const String & typefaceName)437     static LocalRef<jobject> getTypefaceFromAsset (const String& typefaceName)
438     {
439         auto* env = getEnv();
440 
441         LocalRef<jobject> assetManager (env->CallObjectMethod (getAppContext().get(), AndroidContext.getAssets));
442 
443         if (assetManager == nullptr)
444             return LocalRef<jobject>();
445 
446         auto assetTypeface = env->CallStaticObjectMethod (TypefaceClass, TypefaceClass.createFromAsset, assetManager.get(),
447                                                           javaString ("fonts/" + typefaceName).get());
448 
449         // this may throw
450         if (env->ExceptionCheck() != 0)
451         {
452             env->ExceptionClear();
453             return LocalRef<jobject>();
454         }
455 
456         return LocalRef<jobject> (assetTypeface);
457     }
458 
getCacheDirectory()459     static File getCacheDirectory()
460     {
461         static File result = []()
462         {
463             auto appContext = getAppContext();
464 
465             if (appContext != nullptr)
466             {
467                 auto* env = getEnv();
468 
469                 LocalRef<jobject> cacheFile (env->CallObjectMethod (appContext.get(), AndroidContext.getCacheDir));
470                 LocalRef<jstring> jPath ((jstring) env->CallObjectMethod (cacheFile.get(), JavaFile.getAbsolutePath));
471 
472                 return File (juceString (env, jPath.get()));
473             }
474 
475             jassertfalse;
476             return File();
477         } ();
478 
479         return result;
480     }
481 
getInMemoryFontCache()482     static HashMap<String, File>& getInMemoryFontCache()
483     {
484         static HashMap<String, File> cache;
485         return cache;
486     }
487 
getCacheFileForData(const void * data,size_t size)488     static File getCacheFileForData (const void* data, size_t size)
489     {
490         static CriticalSection cs;
491         JNIEnv* const env = getEnv();
492 
493         String key;
494         {
495             LocalRef<jobject> digest (env->CallStaticObjectMethod (JavaMessageDigest, JavaMessageDigest.getInstance, javaString("MD5").get()));
496             LocalRef<jbyteArray> bytes(env->NewByteArray ((int) size));
497 
498             jboolean ignore;
499             auto* jbytes = env->GetByteArrayElements(bytes.get(), &ignore);
500             memcpy(jbytes, data, size);
501             env->ReleaseByteArrayElements(bytes.get(), jbytes, 0);
502 
503             env->CallVoidMethod(digest.get(), JavaMessageDigest.update, bytes.get());
504             LocalRef<jbyteArray> result((jbyteArray) env->CallObjectMethod(digest.get(), JavaMessageDigest.digest));
505 
506             auto* md5Bytes = env->GetByteArrayElements(result.get(), &ignore);
507             key = String::toHexString(md5Bytes, env->GetArrayLength(result.get()), 0);
508             env->ReleaseByteArrayElements(result.get(), md5Bytes, 0);
509         }
510 
511         ScopedLock lock (cs);
512         auto& mapEntry = getInMemoryFontCache().getReference (key);
513 
514         if (mapEntry == File())
515         {
516             mapEntry = getCacheDirectory().getChildFile ("bindata_" + key);
517             mapEntry.replaceWithData (data, size);
518         }
519 
520         return mapEntry;
521     }
522 
523     JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AndroidTypeface)
524 };
525 
526 //==============================================================================
createSystemTypefaceFor(const Font & font)527 Typeface::Ptr Typeface::createSystemTypefaceFor (const Font& font)
528 {
529     return new AndroidTypeface (font);
530 }
531 
createSystemTypefaceFor(const void * data,size_t size)532 Typeface::Ptr Typeface::createSystemTypefaceFor (const void* data, size_t size)
533 {
534     return new AndroidTypeface (data, size);
535 }
536 
scanFolderForFonts(const File &)537 void Typeface::scanFolderForFonts (const File&)
538 {
539     jassertfalse; // not available unless using FreeType
540 }
541 
createNativeLayout(const AttributedString &)542 bool TextLayout::createNativeLayout (const AttributedString&)
543 {
544     return false;
545 }
546 
547 #endif
548 
549 } // namespace juce
550