Add defaults for script/text font pairings and guessing of script based on body contents.

Co-authored-by: Alex Hart <alex@signal.org>
This commit is contained in:
Cody Henthorne
2022-03-30 12:00:29 -04:00
parent 116e711f1a
commit 284a6ae667
15 changed files with 436 additions and 92 deletions
@@ -28,25 +28,25 @@ data class FontManifest(
* @param chineseSimplified Hans Script Fonts
*/
data class FontScripts(
@JsonProperty("latin-extended") val latinExtended: FontScript,
@JsonProperty("cyrillic-extended") val cyrillicExtended: FontScript,
val devanagari: FontScript,
@JsonProperty("chinese-traditional-hk") val chineseTraditionalHk: FontScript,
@JsonProperty("chinese-traditional") val chineseTraditional: FontScript,
@JsonProperty("chinese-simplified") val chineseSimplified: FontScript,
val arabic: FontScript,
val japanese: FontScript,
@JsonProperty("latin-extended") val latinExtended: FontScript?,
@JsonProperty("cyrillic-extended") val cyrillicExtended: FontScript?,
val devanagari: FontScript?,
@JsonProperty("chinese-traditional-hk") val chineseTraditionalHk: FontScript?,
@JsonProperty("chinese-traditional") val chineseTraditional: FontScript?,
@JsonProperty("chinese-simplified") val chineseSimplified: FontScript?,
val arabic: FontScript?,
val japanese: FontScript?,
)
/**
* A collection of fonts for a specific script
*/
data class FontScript(
val regular: String,
val bold: String,
val serif: String,
val script: String,
val condensed: String
val regular: String?,
val bold: String?,
val serif: String?,
val script: String?,
val condensed: String?
)
companion object {
@@ -76,7 +76,7 @@ data class FontManifest(
objectMapper.readValue(it, FontManifest::class.java)
}
} catch (e: Exception) {
Log.w(TAG, "Failed to load manifest from disk")
Log.w(TAG, "Failed to load manifest from disk", e)
return null
}
}
@@ -2,41 +2,44 @@ package org.thoughtcrime.securesms.fonts
import android.content.Context
import android.graphics.Typeface
import android.os.Build
import org.signal.imageeditor.core.Renderer
import org.signal.imageeditor.core.RendererContext
import org.thoughtcrime.securesms.util.FutureTaskListener
import org.thoughtcrime.securesms.util.LocaleUtil
import java.util.Locale
import java.util.concurrent.ExecutionException
/**
* RenderContext TypefaceProvider that provides typefaces using TextFont.
*/
object FontTypefaceProvider : RendererContext.TypefaceProvider {
override fun getSelectedTypeface(context: Context, renderer: Renderer, invalidate: RendererContext.Invalidate): Typeface {
return getTypeface()
// TODO [cody] Need to rework Fonts.kt to not hit network on main, reverting to old typeface for now
// return when (val fontResult = Fonts.resolveFont(context, Locale.getDefault(), TextFont.BOLD)) {
// is Fonts.FontResult.Immediate -> fontResult.typeface
// is Fonts.FontResult.Async -> {
// fontResult.future.addListener(object : FutureTaskListener<Typeface> {
// override fun onSuccess(result: Typeface?) {
// invalidate.onInvalidate(renderer)
// }
//
// override fun onFailure(exception: ExecutionException?) = Unit
// })
//
// fontResult.placeholder
// }
// }
}
class FontTypefaceProvider : RendererContext.TypefaceProvider {
private fun getTypeface(): Typeface {
return if (Build.VERSION.SDK_INT < 26) {
Typeface.create(Typeface.DEFAULT, Typeface.BOLD)
} else {
Typeface.Builder("")
.setFallback("sans-serif")
.setWeight(900)
.build()
private var cachedTypeface: Typeface? = null
private var cachedLocale: Locale? = null
override fun getSelectedTypeface(context: Context, renderer: Renderer, invalidate: RendererContext.Invalidate): Typeface {
val typeface = cachedTypeface
if (typeface != null && cachedLocale == LocaleUtil.getFirstLocale()) {
return typeface
}
return when (val fontResult = Fonts.resolveFont(context, TextFont.BOLD)) {
is Fonts.FontResult.Immediate -> {
cachedTypeface = fontResult.typeface
cachedLocale = LocaleUtil.getFirstLocale()
fontResult.typeface
}
is Fonts.FontResult.Async -> {
fontResult.future.addListener(object : FutureTaskListener<Typeface> {
override fun onSuccess(result: Typeface?) {
invalidate.onInvalidate(renderer)
}
override fun onFailure(exception: ExecutionException?) = Unit
})
fontResult.placeholder
}
}
}
}
@@ -7,6 +7,7 @@ import org.signal.core.util.concurrent.SignalExecutors
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.s3.S3
import org.thoughtcrime.securesms.util.ListenableFutureTask
import org.thoughtcrime.securesms.util.LocaleUtil
import java.io.File
import java.util.Collections
import java.util.Locale
@@ -46,15 +47,16 @@ object Fonts {
}
/**
* Attempts to retrieve a Typeface for the given font / locale combination
* Attempts to retrieve a Typeface for the given font / guessed script and default locales combination
*
* @param context An application context
* @param locale The locale the content will be displayed in
* @param font The desired font
* @param guessedScript The script likely being used based on text content
*
* @return a FontResult that represents either a Typeface or a task retrieving a Typeface.
*/
@WorkerThread
fun resolveFont(context: Context, locale: Locale, font: TextFont): FontResult {
fun resolveFont(context: Context, font: TextFont, guessedScript: SupportedScript = SupportedScript.UNKNOWN): FontResult {
synchronized(this) {
val errorFallback = FontResult.Immediate(Typeface.create(font.fallbackFamily, font.fallbackStyle))
val version = FontVersion.get(context)
@@ -66,11 +68,21 @@ object Fonts {
Log.d(TAG, "Loaded manifest.")
val fontScript = resolveScriptNameFromLocale(locale, manifest) ?: return errorFallback
val localeDefaults: List<Locale> = LocaleUtil.getLocaleDefaults()
val supportedScript: SupportedScript = getSupportedScript(localeDefaults, guessedScript)
val fontScript = resolveFontScriptFromScriptName(supportedScript, manifest)
if (fontScript == null) {
Log.d(TAG, "Manifest does not have an entry for $supportedScript. Using default.")
return FontResult.Immediate(getDefaultFontForScriptAndStyle(supportedScript, font))
}
Log.d(TAG, "Loaded script for locale.")
val fontNetworkPath = getScriptPath(font, fontScript)
if (fontNetworkPath == null) {
Log.d(TAG, "Manifest does not contain a network path for $supportedScript. Using default.")
return FontResult.Immediate(getDefaultFontForScriptAndStyle(supportedScript, font))
}
val fontLocalPath = FontFileMap.getNameOnDisk(context, version, fontNetworkPath)
@@ -80,7 +92,7 @@ object Fonts {
}
val fontDownloadKey = FontDownloadKey(
version, locale, font
version, supportedScript, font
)
val taskInProgress = taskCache[fontDownloadKey]
@@ -93,7 +105,7 @@ object Fonts {
} else {
Log.d(TAG, "Could not find a task in progress. Returning new async.")
val newTask = ListenableFutureTask {
val newLocalPath = downloadFont(context, locale, font, version, manifest)
val newLocalPath = downloadFont(context, supportedScript, font, version, manifest)
Log.d(TAG, "Finished download, $newLocalPath")
val typeface = newLocalPath?.let { loadFontIntoTypeface(context, version, it) } ?: errorFallback.typeface
@@ -112,6 +124,69 @@ object Fonts {
}
}
private fun getDefaultFontForScriptAndStyle(supportedScript: SupportedScript, font: TextFont): Typeface {
return when (supportedScript) {
SupportedScript.CYRILLIC -> {
when (font) {
TextFont.REGULAR -> Typeface.SANS_SERIF
TextFont.BOLD -> Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD)
TextFont.SERIF -> Typeface.SERIF
TextFont.SCRIPT -> TypefaceHelper.typefaceFor(TypefaceHelper.Family.SERIF, "semibold", TypefaceHelper.Weight.SEMI_BOLD)
TextFont.CONDENSED -> TypefaceHelper.typefaceFor(TypefaceHelper.Family.SANS_SERIF, "light", TypefaceHelper.Weight.LIGHT)
}
}
SupportedScript.DEVANAGARI -> {
when (font) {
TextFont.REGULAR -> Typeface.SANS_SERIF
TextFont.BOLD -> Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD)
TextFont.SERIF -> Typeface.SANS_SERIF
TextFont.SCRIPT -> Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD)
TextFont.CONDENSED -> TypefaceHelper.typefaceFor(TypefaceHelper.Family.SANS_SERIF, "light", TypefaceHelper.Weight.LIGHT)
}
}
SupportedScript.CHINESE_TRADITIONAL_HK,
SupportedScript.CHINESE_TRADITIONAL,
SupportedScript.CHINESE_SIMPLIFIED,
SupportedScript.UNKNOWN_CJK -> {
when (font) {
TextFont.REGULAR -> Typeface.SANS_SERIF
TextFont.BOLD -> TypefaceHelper.typefaceFor(TypefaceHelper.Family.SANS_SERIF, "semibold", TypefaceHelper.Weight.SEMI_BOLD)
TextFont.SERIF -> TypefaceHelper.typefaceFor(TypefaceHelper.Family.SANS_SERIF, "thin", TypefaceHelper.Weight.THIN)
TextFont.SCRIPT -> TypefaceHelper.typefaceFor(TypefaceHelper.Family.SANS_SERIF, "light", TypefaceHelper.Weight.LIGHT)
TextFont.CONDENSED -> TypefaceHelper.typefaceFor(TypefaceHelper.Family.SANS_SERIF, "demilight", TypefaceHelper.Weight.DEMI_LIGHT)
}
}
SupportedScript.ARABIC -> {
when (font) {
TextFont.REGULAR -> Typeface.SANS_SERIF
TextFont.BOLD -> Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD)
TextFont.SERIF -> Typeface.SERIF
TextFont.SCRIPT -> Typeface.create(Typeface.SERIF, Typeface.BOLD)
TextFont.CONDENSED -> TypefaceHelper.typefaceFor(TypefaceHelper.Family.SANS_SERIF, "black", TypefaceHelper.Weight.BLACK)
}
}
SupportedScript.JAPANESE -> {
when (font) {
TextFont.REGULAR -> Typeface.SANS_SERIF
TextFont.BOLD -> Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD)
TextFont.SERIF -> Typeface.SERIF
TextFont.SCRIPT -> Typeface.create(Typeface.SERIF, Typeface.BOLD)
TextFont.CONDENSED -> TypefaceHelper.typefaceFor(TypefaceHelper.Family.SANS_SERIF, "medium", TypefaceHelper.Weight.MEDIUM)
}
}
SupportedScript.LATIN,
SupportedScript.UNKNOWN -> {
when (font) {
TextFont.REGULAR -> Typeface.SANS_SERIF
TextFont.BOLD -> Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD)
TextFont.SERIF -> Typeface.SERIF
TextFont.SCRIPT -> Typeface.create(Typeface.SERIF, Typeface.BOLD)
TextFont.CONDENSED -> TypefaceHelper.typefaceFor(TypefaceHelper.Family.SANS_SERIF, "black", TypefaceHelper.Weight.BLACK)
}
}
}
}
@WorkerThread
private fun loadFontIntoTypeface(context: Context, fontVersion: FontVersion, fontLocalPath: String): Typeface? {
return try {
@@ -146,9 +221,13 @@ object Fonts {
* Downloads the given font file from S3
*/
@WorkerThread
private fun downloadFont(context: Context, locale: Locale, font: TextFont, fontVersion: FontVersion, fontManifest: FontManifest): String? {
val script: FontManifest.FontScript = resolveScriptNameFromLocale(locale, fontManifest) ?: return null
val path = getScriptPath(font, script)
private fun downloadFont(context: Context, supportedScript: SupportedScript?, font: TextFont, fontVersion: FontVersion, fontManifest: FontManifest): String? {
if (supportedScript == null) {
return null
}
val script: FontManifest.FontScript = resolveFontScriptFromScriptName(supportedScript, fontManifest) ?: return null
val path = getScriptPath(font, script) ?: return null
val networkPath = "$BASE_STATIC_BUCKET_URL/${fontVersion.id}/$path"
val localUUID = UUID.randomUUID().toString()
val localPath = "${fontVersion.path}/" + localUUID
@@ -162,7 +241,7 @@ object Fonts {
}
}
private fun getScriptPath(font: TextFont, script: FontManifest.FontScript): String {
private fun getScriptPath(font: TextFont, script: FontManifest.FontScript): String? {
return when (font) {
TextFont.REGULAR -> script.regular
TextFont.BOLD -> script.bold
@@ -172,22 +251,62 @@ object Fonts {
}
}
private fun resolveScriptNameFromLocale(locale: Locale, fontManifest: FontManifest): FontManifest.FontScript? {
val fontScript: FontManifest.FontScript = when (ScriptUtil.getScript(locale).apply { Log.d(TAG, "Getting Script for $this") }) {
ScriptUtil.LATIN -> fontManifest.scripts.latinExtended
ScriptUtil.ARABIC -> fontManifest.scripts.arabic
ScriptUtil.CHINESE_SIMPLIFIED -> fontManifest.scripts.chineseSimplified
ScriptUtil.CHINESE_TRADITIONAL -> fontManifest.scripts.chineseTraditional
ScriptUtil.CYRILLIC -> fontManifest.scripts.cyrillicExtended
ScriptUtil.DEVANAGARI -> fontManifest.scripts.devanagari
ScriptUtil.JAPANESE -> fontManifest.scripts.japanese
else -> return null
private fun getSupportedScript(locales: List<Locale>, guessedScript: SupportedScript): SupportedScript {
if (guessedScript != SupportedScript.UNKNOWN && guessedScript != SupportedScript.UNKNOWN_CJK) {
return guessedScript
} else if (guessedScript == SupportedScript.UNKNOWN_CJK) {
val likelyScript: SupportedScript? = locales.mapNotNull {
try {
when (it.isO3Country) {
"HKG" -> SupportedScript.CHINESE_TRADITIONAL_HK
"CHN" -> SupportedScript.CHINESE_SIMPLIFIED
"TWN" -> SupportedScript.CHINESE_TRADITIONAL
"JPN" -> SupportedScript.JAPANESE
else -> null
}
} catch (e: java.util.MissingResourceException) {
Log.w(TAG, "Unable to get ISO-3 country code for: $it")
null
}
}.firstOrNull()
if (likelyScript != null) {
return likelyScript
}
}
return if (fontScript == fontManifest.scripts.chineseSimplified && locale.isO3Country == "HKG") {
fontManifest.scripts.chineseTraditionalHk
val locale = locales.first()
val supportedScript: SupportedScript = when (ScriptUtil.getScript(locale).also { Log.d(TAG, "Getting Script for $it") }) {
ScriptUtil.LATIN -> SupportedScript.LATIN
ScriptUtil.ARABIC -> SupportedScript.ARABIC
ScriptUtil.CHINESE_SIMPLIFIED -> SupportedScript.CHINESE_SIMPLIFIED
ScriptUtil.CHINESE_TRADITIONAL -> SupportedScript.CHINESE_TRADITIONAL
ScriptUtil.CYRILLIC -> SupportedScript.CYRILLIC
ScriptUtil.DEVANAGARI -> SupportedScript.DEVANAGARI
ScriptUtil.JAPANESE -> SupportedScript.JAPANESE
else -> SupportedScript.UNKNOWN
}
return if (supportedScript == SupportedScript.CHINESE_SIMPLIFIED && locale.isO3Country == "HKG") {
SupportedScript.CHINESE_TRADITIONAL_HK
} else {
fontScript
supportedScript
}
}
private fun resolveFontScriptFromScriptName(supportedScript: SupportedScript?, fontManifest: FontManifest): FontManifest.FontScript? {
return when (supportedScript.also { Log.d(TAG, "Getting Script for $it") }) {
SupportedScript.LATIN -> fontManifest.scripts.latinExtended
SupportedScript.ARABIC -> fontManifest.scripts.arabic
SupportedScript.CHINESE_SIMPLIFIED -> fontManifest.scripts.chineseSimplified
SupportedScript.CHINESE_TRADITIONAL -> fontManifest.scripts.chineseTraditional
SupportedScript.CYRILLIC -> fontManifest.scripts.cyrillicExtended
SupportedScript.DEVANAGARI -> fontManifest.scripts.devanagari
SupportedScript.JAPANESE -> fontManifest.scripts.japanese
SupportedScript.CHINESE_TRADITIONAL_HK -> fontManifest.scripts.chineseTraditionalHk
SupportedScript.UNKNOWN_CJK -> null
SupportedScript.UNKNOWN -> null
null -> null
}
}
@@ -201,7 +320,7 @@ object Fonts {
private data class FontDownloadKey(
val version: FontVersion,
val locale: Locale,
val script: SupportedScript,
val font: TextFont
)
}
@@ -0,0 +1,17 @@
package org.thoughtcrime.securesms.fonts
/**
* Scripts with font support for Stories
*/
enum class SupportedScript {
LATIN,
CYRILLIC,
DEVANAGARI,
CHINESE_TRADITIONAL_HK,
CHINESE_TRADITIONAL,
CHINESE_SIMPLIFIED,
UNKNOWN_CJK,
ARABIC,
JAPANESE,
UNKNOWN
}
@@ -0,0 +1,50 @@
package org.thoughtcrime.securesms.fonts
/**
* Attempt to guess the script based on the unicode characters used. The script
* with the most use in a string will be picked. Tie goes to [SupportedScript.LATIN].
* Unicode does not cleanly separate Hong Kong and Chinese character ranges so some
* other means must be used to distinguish it (e.g., Locale).
*/
object TextToScript {
private val LATIN_RANGES: List<IntRange> = listOf(0x0000..0x024F, 0x1E00..0x1EFF, 0x2C60..0x2C7F, 0xA720..0xA7FF, 0xAB30..0xAB6F)
private val CYRILLIC_RANGES: List<IntRange> = listOf(0x0400..0x04FF, 0x0500..0x052F, 0x1C80..0x1C8F, 0x2DE0..0x2DFF, 0xA640..0xA69F)
private val DEVANAGARI_RANGES: List<IntRange> = listOf(0x0900..0x097F, 0xA8E0..0xA8FF, 0x30A0..0x30FF)
private val CJK_RANGES: List<IntRange> = listOf(0x31C0..0x31EF, 0x3300..0x33FF, 0x4E00..0x9FFF, 0xF900..0xFAFF, 0xFE30..0xFE4F, 0x20000..0x2EBEF, 0x2F800..0x2FA1F)
private val CJK_JAPANESE_RANGES: List<IntRange> = listOf(0x3040..0x309F, 0x30A0..0x30FF, 0x3190..0x319F)
private val ARABIC_RANGES: List<IntRange> = listOf(0x0600..0x06FF, 0x0750..0x077F, 0x0870..0x089F, 0x08A0..0x08FF)
private val allRanges = mapOf(
SupportedScript.LATIN to LATIN_RANGES,
SupportedScript.CYRILLIC to CYRILLIC_RANGES,
SupportedScript.DEVANAGARI to DEVANAGARI_RANGES,
SupportedScript.UNKNOWN_CJK to CJK_RANGES,
SupportedScript.JAPANESE to CJK_JAPANESE_RANGES,
SupportedScript.ARABIC to ARABIC_RANGES
)
fun guessScript(text: CharSequence): SupportedScript {
val scriptCounts: MutableMap<SupportedScript, Int> = SupportedScript.values().associate { it to 0 }.toMutableMap()
val input = text.toString()
for (i in 0 until input.codePointCount(0, input.length)) {
val codePoint = input.codePointAt(i)
for ((script, ranges) in allRanges) {
if (ranges.contains(codePoint)) {
scriptCounts[script] = scriptCounts[script]!! + 1
}
}
}
val most: SupportedScript = scriptCounts.maxByOrNull { it.value }?.key ?: SupportedScript.UNKNOWN
return if (most == SupportedScript.UNKNOWN_CJK && scriptCounts[SupportedScript.JAPANESE]!! > 0) {
SupportedScript.JAPANESE
} else {
most
}
}
private fun List<IntRange>.contains(x: Int): Boolean = any { x in it }
}
@@ -0,0 +1,39 @@
package org.thoughtcrime.securesms.fonts
import android.graphics.Typeface
import android.os.Build
/**
* API independent, best-effort way of creating [Typeface]s of various family/weight.
*/
object TypefaceHelper {
fun typefaceFor(family: Family, weightName: String, weight: Weight): Typeface {
return when {
Build.VERSION.SDK_INT >= 28 -> Typeface.create(
Typeface.create(family.familyName, Typeface.NORMAL),
weight.value,
false
)
Build.VERSION.SDK_INT >= 21 -> Typeface.create("${family.familyName}-$weightName", Typeface.NORMAL)
else -> Typeface.create(family.familyName, if (weight.value > Weight.MEDIUM.value) Typeface.BOLD else Typeface.NORMAL)
}
}
enum class Family(val familyName: String) {
SANS_SERIF("sans-serif"),
SERIF("serif")
}
enum class Weight(val value: Int) {
THIN(100),
EXTRA_LIGHT(200),
DEMI_LIGHT(200),
LIGHT(300),
NORMAL(400),
MEDIUM(500),
SEMI_BOLD(600),
BOLD(700),
EXTRA_BOLD(800),
BLACK(900),
}
}
@@ -8,7 +8,6 @@ import org.thoughtcrime.securesms.jobmanager.Data
import org.thoughtcrime.securesms.jobmanager.Job
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
import org.thoughtcrime.securesms.util.FutureTaskListener
import java.util.Locale
import java.util.concurrent.CountDownLatch
import java.util.concurrent.ExecutionException
import java.util.concurrent.TimeUnit
@@ -41,10 +40,8 @@ class FontDownloaderJob private constructor(parameters: Parameters) : BaseJob(pa
override fun onFailure() = Unit
override fun onRun() {
val locale = Locale.getDefault()
val asyncResults = TextFont.values()
.map { Fonts.resolveFont(context, locale, it) }
.map { Fonts.resolveFont(context, it) }
.filterIsInstance(Fonts.FontResult.Async::class.java)
if (asyncResults.isEmpty()) {
@@ -41,7 +41,7 @@ public final class ImageEditorModelRenderMediaTransform implements MediaTransfor
public @NonNull Media transform(@NonNull Context context, @NonNull Media media) {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
Bitmap bitmap = modelToRender.render(context, size, FontTypefaceProvider.INSTANCE);
Bitmap bitmap = modelToRender.render(context, size, new FontTypefaceProvider());
try {
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outputStream);
@@ -16,15 +16,16 @@ import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.fonts.Fonts
import org.thoughtcrime.securesms.fonts.TextFont
import org.thoughtcrime.securesms.fonts.TextToScript
import org.thoughtcrime.securesms.util.FutureTaskListener
import org.thoughtcrime.securesms.util.livedata.Store
import java.util.Locale
import java.util.concurrent.ExecutionException
class TextStoryPostCreationViewModel : ViewModel() {
private val store = Store(TextStoryPostCreationState())
private val textFontSubject: Subject<TextFont> = BehaviorSubject.create()
private val temporaryBodySubject: Subject<String> = BehaviorSubject.createDefault("")
private val disposables = CompositeDisposable()
private val internalThumbnail = MutableLiveData<Bitmap>()
@@ -38,14 +39,16 @@ class TextStoryPostCreationViewModel : ViewModel() {
init {
textFontSubject.onNext(store.state.textFont)
textFontSubject
val scriptGuess = temporaryBodySubject.observeOn(Schedulers.io()).map { TextToScript.guessScript(it) }
Observable.combineLatest(textFontSubject, scriptGuess, ::Pair)
.observeOn(Schedulers.io())
.distinctUntilChanged()
.map { Fonts.resolveFont(ApplicationDependencies.getApplication(), Locale.getDefault(), it) }
.switchMap {
when (it) {
is Fonts.FontResult.Async -> asyncFontEmitter(it)
is Fonts.FontResult.Immediate -> Observable.just(it.typeface)
.map { (textFont, script) -> Fonts.resolveFont(ApplicationDependencies.getApplication(), textFont, script) }
.switchMap { result ->
when (result) {
is Fonts.FontResult.Async -> asyncFontEmitter(result)
is Fonts.FontResult.Immediate -> Observable.just(result.typeface)
}
}
.subscribeOn(Schedulers.io())
@@ -142,6 +145,10 @@ class TextStoryPostCreationViewModel : ViewModel() {
store.update { it.copy(linkPreviewUri = url) }
}
fun setTemporaryBody(temporaryBody: String) {
temporaryBodySubject.onNext(temporaryBody)
}
companion object {
private val TAG = Log.tag(TextStoryPostCreationViewModel::class.java)
private const val TEXT_STORY_INSTANCE_STATE = "text.story.instance.state"
@@ -20,6 +20,7 @@ import androidx.appcompat.widget.AppCompatSeekBar
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.ConstraintSet
import androidx.core.view.updateLayoutParams
import androidx.core.widget.doAfterTextChanged
import androidx.core.widget.doOnTextChanged
import androidx.fragment.app.viewModels
import androidx.transition.TransitionManager
@@ -112,6 +113,9 @@ class TextStoryPostTextEntryFragment : KeyboardEntryDialogFragment(
input.doOnTextChanged { _, _, _, _ ->
presentHint()
}
input.doAfterTextChanged { text ->
viewModel.setTemporaryBody(text?.toString() ?: "")
}
input.setText(viewModel.getBody())
}
@@ -219,7 +219,7 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu
imageEditorHud = view.findViewById(R.id.scribble_hud);
imageEditorView = view.findViewById(R.id.image_editor_view);
imageEditorView.setTypefaceProvider(FontTypefaceProvider.INSTANCE);
imageEditorView.setTypefaceProvider(new FontTypefaceProvider());
int width = getResources().getDisplayMetrics().widthPixels;
int height = (int) ((16 / 9f) * width);
@@ -555,7 +555,7 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu
FaceDetector detector = new AndroidFaceDetector();
Point size = model.getOutputSizeMaxWidth(1000);
Bitmap render = model.render(ApplicationDependencies.getApplication(), size, FontTypefaceProvider.INSTANCE);
Bitmap render = model.render(ApplicationDependencies.getApplication(), size, new FontTypefaceProvider());
try {
return new FaceDetectionResult(detector.detect(render), new Point(render.getWidth(), render.getHeight()), inverseCropPosition);
} finally {
@@ -769,7 +769,7 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu
@WorkerThread
public @NonNull Uri renderToSingleUseBlob() {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
Bitmap image = imageEditorView.getModel().render(requireContext(), FontTypefaceProvider.INSTANCE);
Bitmap image = imageEditorView.getModel().render(requireContext(), new FontTypefaceProvider());
image.compress(Bitmap.CompressFormat.JPEG, 80, outputStream);
image.recycle();
@@ -17,6 +17,7 @@ import org.thoughtcrime.securesms.conversation.colors.ChatColors
import org.thoughtcrime.securesms.database.model.databaseprotos.StoryTextPost
import org.thoughtcrime.securesms.fonts.Fonts
import org.thoughtcrime.securesms.fonts.TextFont
import org.thoughtcrime.securesms.fonts.TextToScript
import org.thoughtcrime.securesms.linkpreview.LinkPreview
import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel
import org.thoughtcrime.securesms.mediasend.v2.text.TextAlignment
@@ -25,7 +26,6 @@ import org.thoughtcrime.securesms.mediasend.v2.text.TextStoryScale
import org.thoughtcrime.securesms.mediasend.v2.text.TextStoryTextWatcher
import org.thoughtcrime.securesms.stories.viewer.page.StoryDisplay
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture
import org.thoughtcrime.securesms.util.concurrent.SimpleTask
import org.thoughtcrime.securesms.util.visible
import java.util.Locale
@@ -153,15 +153,10 @@ class StoryTextPostView @JvmOverloads constructor(
setTextBackgroundColor(storyTextPost.textBackgroundColor)
setTextGravity(TextAlignment.CENTER)
SimpleTask.run(
{
when (val fontResult = Fonts.resolveFont(context, Locale.getDefault(), font)) {
is Fonts.FontResult.Immediate -> fontResult.typeface
is Fonts.FontResult.Async -> fontResult.future.get()
}
},
{ typeface -> setTypeface(typeface) }
)
when (val fontResult = Fonts.resolveFont(context, font, TextToScript.guessScript(storyTextPost.body))) {
is Fonts.FontResult.Immediate -> setTypeface(fontResult.typeface)
is Fonts.FontResult.Async -> setTypeface(fontResult.future.get())
}
hideCloseButton()
@@ -0,0 +1,33 @@
package org.thoughtcrime.securesms.util
import androidx.core.os.LocaleListCompat
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.dynamiclanguage.LanguageString
import java.util.Locale
object LocaleUtil {
fun getFirstLocale(): Locale {
return getLocaleDefaults().firstOrNull() ?: Locale.getDefault()
}
/**
* Get a user priority list of locales supported on the device, with the locale set via Signal settings
* as highest priority over system settings.
*/
fun getLocaleDefaults(): List<Locale> {
val locales: MutableList<Locale> = mutableListOf()
val signalLocale: Locale? = LanguageString.parseLocale(SignalStore.settings().language)
val localeList: LocaleListCompat = LocaleListCompat.getDefault()
if (signalLocale != null) {
locales += signalLocale
}
for (index in 0 until localeList.size()) {
locales += localeList.get(index)
}
return locales
}
}
@@ -50,7 +50,7 @@ final class WallpaperCropViewModel extends ViewModel {
{
SignalExecutors.BOUNDED.execute(
() -> {
Bitmap bitmap = model.render(context, size, FontTypefaceProvider.INSTANCE);
Bitmap bitmap = model.render(context, size, new FontTypefaceProvider());
try {
ChatWallpaper chatWallpaper = repository.setWallPaper(BitmapUtil.toWebPByteArray(bitmap));
callback.onComplete(chatWallpaper);
@@ -0,0 +1,80 @@
package org.thoughtcrime.securesms.fonts
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
@RunWith(Parameterized::class)
class TextToScriptTest(
private val texts: List<CharSequence>,
private val guess: SupportedScript
) {
@Test
fun guessScript() {
for (text in texts) {
assertEquals("Expecting $guess for $text", guess, TextToScript.guessScript(text))
}
}
companion object {
@JvmStatic
@Parameterized.Parameters(name = "{index}: guessLocale(..) = {1}")
fun data(): Iterable<Array<Any>> = arrayListOf(
arrayOf(
listOf("Whats up?", "I really appreciate it.", "How wonderful!", "Lets grab a bite to eat.", "Im gonna hit the sack.", "More latin than other Прошу"),
SupportedScript.LATIN
),
arrayOf(
listOf("Guten Tag", "Sprechen Sie Englisch?", "Gut, danke", "Gibt es ein Restaurant in der Nähe?", "Haben Sie noch Zimmer frei?", "Haben Sie noch Zimmer frei? Прошу"),
SupportedScript.LATIN
),
arrayOf(
listOf("Bom Dia", "Prazer", "Qual é o seu nome?", "Como vai?", "Que horas são?", "Que horas são? Прошу"),
SupportedScript.LATIN
),
arrayOf(
listOf("Bună ziua.", "Cum te numeşti?", " Îmi pare rău.", "Unde este toaleta?", "Felicitări!", "Multumesc pentru tot ajutorul Clauz", "Felicitări! Прошу"),
SupportedScript.LATIN
),
arrayOf(
listOf("добрий день", "добрий день", "привіт", "дякую", "дякую", "будь ласка", "дуже добре", "Вибачте", "Так", "па-па", "Ви говорите англійською?", "Вибачте abc"),
SupportedScript.CYRILLIC
),
arrayOf(
listOf("Да", "Нет", "Пожалуйста", "Спасибо", "Не за что.", "на здоровье", "Прошу прощения.", "Извините.", "Я не понимаю.", "Я не говорю по-Русски.", "Я не понимаю. abc"),
SupportedScript.CYRILLIC
),
arrayOf(
listOf("स्वागत", "नमस्ते", "तुम कैसे हो?", "मैं अच्छा हूँ, धन्यवाद। और तुम?", "तुम कहाँ से (आए) हो?", "शुभ रात्रि", "धन्यवाद", "धन्यवाद abc"),
SupportedScript.DEVANAGARI
),
arrayOf(
listOf("食咗飯未呀?", "唔該", "多謝", "太貴啦", "好味", "我花生過敏", "地鐵站係邊", "廁所係邊", "無問題", "無問題a"),
SupportedScript.UNKNOWN_CJK
),
arrayOf(
listOf("你好嗎", "很高興認識你", "我不會說漢語", "我需要你的幫助", "我丟了手提包", "我丟了手提包abc"),
SupportedScript.UNKNOWN_CJK
),
arrayOf(
listOf("你好", "请问", "你会说英语吗", "我听不懂", "洗手间在哪里", "我可以用您的手机吗", "我可以用您的手机吗abcd"),
SupportedScript.UNKNOWN_CJK
),
arrayOf(
listOf("はい", "いいえ", "こんばんは", "えいごをはなせますか", "おあいできて うれしいです", "もっと ゆっくりはなしてください", "だいじょうぶです", "わかります", "だいじょうぶです123"),
SupportedScript.JAPANESE
),
arrayOf(
listOf("ما اسمك؟", "كيف حالك؟", "انا آسف", "أين الحمام؟", "هل يمكنك التحدث بشكل أبطأ من فضلك؟", "مندواعي سروري مقابلتك", "كيفتجري الامور؟", "أنا تائه.", "أنا تائه.12"),
SupportedScript.ARABIC
),
arrayOf(
listOf("ППППaaaa", ""),
SupportedScript.LATIN
),
)
}
}