package org.thoughtcrime.securesms.emoji import android.net.Uri import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.ObjectMapper import org.thoughtcrime.securesms.components.emoji.CompositeEmojiPageModel import org.thoughtcrime.securesms.components.emoji.Emoji import org.thoughtcrime.securesms.components.emoji.EmojiPageModel import org.thoughtcrime.securesms.components.emoji.StaticEmojiPageModel import org.thoughtcrime.securesms.util.Hex import java.io.InputStream import java.nio.charset.Charset typealias UriFactory = (sprite: String, format: String) -> Uri /** * Takes an emoji_data.json file data and parses it into an EmojiSource */ object EmojiJsonParser { private val OBJECT_MAPPER = ObjectMapper() private const val ESTIMATED_EMOJI_COUNT = 3500 @JvmStatic fun verify(body: InputStream) { parse(body) { _, _ -> Uri.EMPTY }.getOrThrow() } fun parse(body: InputStream, uriFactory: UriFactory): Result { return try { Result.success(buildEmojiSourceFromNode(OBJECT_MAPPER.readTree(body), uriFactory)) } catch (e: Exception) { Result.failure(e) } } private fun buildEmojiSourceFromNode(node: JsonNode, uriFactory: UriFactory): ParsedEmojiData { val format: String = node["format"].textValue() val obsolete: List = node["obsolete"].toObseleteList() val dataPages: List = getDataPages(format, node["emoji"], uriFactory) val jumboPages: Map = getJumboPages(node["jumbomoji"]) val displayPages: List = mergeToDisplayPages(dataPages) val metrics: EmojiMetrics = node["metrics"].toEmojiMetrics() val densities: List = node["densities"].toDensityList() return ParsedEmojiData(metrics, densities, format, displayPages, dataPages, jumboPages, obsolete) } private fun getDataPages(format: String, emoji: JsonNode, uriFactory: UriFactory): List { return emoji.fields() .asSequence() .sortedWith { lhs, rhs -> val lhsCategory = EmojiCategory.forKey(lhs.key.asCategoryKey()) val rhsCategory = EmojiCategory.forKey(rhs.key.asCategoryKey()) val comp = lhsCategory.priority.compareTo(rhsCategory.priority) if (comp == 0) { val lhsIndex = lhs.key.getPageIndex() val rhsIndex = rhs.key.getPageIndex() lhsIndex.compareTo(rhsIndex) } else { comp } } .map { createPage(it.key, format, it.value, uriFactory) } .toList() } private fun getJumboPages(jumbo: JsonNode?): Map { if (jumbo != null) { return jumbo.fields() .asSequence() .map { (page: String, node: JsonNode) -> node.associate { it.textValue() to page } } .flatMap { it.entries } .associateTo(HashMap(ESTIMATED_EMOJI_COUNT)) { it.key to it.value } } return emptyMap() } private fun createPage(pageName: String, format: String, page: JsonNode, uriFactory: UriFactory): EmojiPageModel { val category = EmojiCategory.forKey(pageName.asCategoryKey()) val pageList = page.mapIndexed { i, data -> if (data.size() == 0) { throw IllegalStateException("Page index $pageName.$i had no data") } else { val variations: MutableList = mutableListOf() val rawVariations: MutableList = mutableListOf() data.forEach { variations += it.textValue().encodeAsUtf16() rawVariations += it.textValue() } Emoji(variations, rawVariations) } } return StaticEmojiPageModel(category, pageList, uriFactory(pageName, format)) } private fun mergeToDisplayPages(dataPages: List): List { return dataPages.groupBy { it.iconAttr } .map { (icon, pages) -> if (pages.size <= 1) pages.first() else CompositeEmojiPageModel(icon, pages) } } } private fun JsonNode?.toObseleteList(): List { return if (this == null) { listOf() } else { map { node -> ObsoleteEmoji(node["obsoleted"].textValue().encodeAsUtf16(), node["replace_with"].textValue().encodeAsUtf16()) }.toList() } } private fun JsonNode.toEmojiMetrics(): EmojiMetrics { return EmojiMetrics(this["raw_width"].asInt(), this["raw_height"].asInt(), this["per_row"].asInt()) } private fun JsonNode.toDensityList(): List { return map { it.textValue() } } private fun String.encodeAsUtf16() = String(Hex.fromStringCondensed(this), Charset.forName("UTF-16")) private fun String.asCategoryKey() = replace("(_\\d+)*$".toRegex(), "") private fun String.getPageIndex() = "^.*_(\\d+)+$".toRegex().find(this)?.let { it.groupValues[1] }?.toInt() ?: throw IllegalStateException("No index.") data class ParsedEmojiData( override val metrics: EmojiMetrics, override val densities: List, override val format: String, override val displayPages: List, override val dataPages: List, override val jumboPages: Map, override val obsolete: List ) : EmojiData