Skip to content

Commit

Permalink
EXPERIMENTAL: Implement cache for profile assets
Browse files Browse the repository at this point in the history
  • Loading branch information
WinG4merBR committed Jan 5, 2025
1 parent 8bca9db commit 3f600c4
Show file tree
Hide file tree
Showing 13 changed files with 562 additions and 44 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package net.cakeyfox.serializable.data

import kotlinx.serialization.Serializable

@Serializable
data class ImagePosition(
val x: Float,
val y: Float,
val arc: Arc? = null,
)

@Serializable
data class Arc(
val x: Float,
val y: Float,
val radius: Int,
)
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package net.cakeyfox.serializable.database.data

import kotlinx.serialization.Serializable
import net.cakeyfox.serializable.data.ImagePosition

@Serializable
data class Layout(
Expand Down Expand Up @@ -40,27 +41,14 @@ data class FontSize(

@Serializable
data class Positions(
val avatarPosition: Position,
val usernamePosition: Position,
val aboutmePosition: Position,
val marriedPosition: Position,
val marriedSincePosition: Position,
val marriedUsernamePosition: Position,
val badgesPosition: Position,
val decorationPosition: Position,
val cakesPosition: Position,
val avatarPosition: ImagePosition,
val usernamePosition: ImagePosition,
val aboutmePosition: ImagePosition,
val marriedPosition: ImagePosition,
val marriedSincePosition: ImagePosition,
val marriedUsernamePosition: ImagePosition,
val badgesPosition: ImagePosition,
val decorationPosition: ImagePosition,
val cakesPosition: ImagePosition,
)

@Serializable
data class Position(
val x: Float,
val y: Float,
val arc: Arc? = null,
)

@Serializable
data class Arc(
val x: Float,
val y: Float,
val radius: Int,
)
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ class PayExecutor : FoxyCommandExecutor() {

it.edit {
content = pretty(
FoxyEmotes.FoxyCry,
FoxyEmotes.FoxyYay,
context.locale["pay.success", amount.toString(), userToPay.asMention]
)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package net.cakeyfox.foxy.command.vanilla.social

import net.cakeyfox.common.Colors
import net.cakeyfox.common.FoxyEmotes
import net.cakeyfox.foxy.command.FoxyInteractionContext
import net.cakeyfox.foxy.command.structure.FoxyCommandExecutor
import net.cakeyfox.foxy.utils.image.ImageUtils
import net.cakeyfox.foxy.utils.pretty
import net.cakeyfox.foxy.utils.profile.FoxyProfileRender
import net.cakeyfox.foxy.utils.profile.ProfileRender
import net.cakeyfox.foxy.utils.profile.config.ProfileConfig
import net.dv8tion.jda.api.entities.User
import net.dv8tion.jda.api.utils.FileUpload

Expand All @@ -13,8 +17,30 @@ class ProfileViewExecutor: FoxyCommandExecutor() {
context.defer()

val user = context.getOption<User>("user") ?: context.event.user
val userData = context.db.utils.user.getDiscordUser(user.id)

val profile = FoxyProfileRender(context).create(user) ?: return
if (userData.isBanned == true) {
context.reply {
embed {
title = pretty(FoxyEmotes.FoxyRage, context.locale["profile.isBanned", user.name])
color = Colors.RED
field {
name = pretty(FoxyEmotes.FoxyDrinkingCoffee, context.locale["profile.banReason"])
value = userData.banReason ?: context.locale["profile.noBanReasonProvided"]
inline = false
}

field {
name = pretty(FoxyEmotes.FoxyBan, context.locale["profile.bannedSince"])
value = userData.banDate?.let { context.utils.convertISOToDiscordTimestamp(it) }.toString()
inline = false
}
}
}
return
}

val profile = ProfileRender(ProfileConfig(1436, 884), context).create(user, userData)
val file = FileUpload.fromData(profile, "profile.png")

context.reply {
Expand Down
79 changes: 79 additions & 0 deletions foxy/src/main/kotlin/net/cakeyfox/foxy/utils/image/ImageUtils.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package net.cakeyfox.foxy.utils.image

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import mu.KotlinLogging
import net.cakeyfox.foxy.utils.profile.ProfileCacheManager
import net.cakeyfox.serializable.data.ImagePosition
import java.awt.Color
import java.awt.Font
import java.awt.Graphics2D
import java.awt.image.BufferedImage
import java.io.InputStream
import java.net.URL
import javax.imageio.ImageIO
import kotlin.reflect.jvm.jvmName

object ImageUtils {
private val logger = KotlinLogging.logger(this::class.jvmName)

fun getFont(fontName: String, fontSize: Int): Font? {
val fontStream: InputStream? = this::class.java.classLoader.getResourceAsStream("fonts/$fontName.ttf")

return if (fontStream != null) {
try {
Font.createFont(Font.TRUETYPE_FONT, fontStream).deriveFont(Font.PLAIN, fontSize.toFloat())
} catch (e: Exception) {
logger.error(e) { "Can't load font $fontName " }
null
}
} else {
logger.error { "$fontName font not found on resources path" }
null
}
}

suspend fun loadImageFromURL(url: String): BufferedImage {
return withContext(Dispatchers.IO) {
try {
ProfileCacheManager.imageCache.get(url) {
downloadImage(url)
}
} catch (e: Exception) {
logger.error(e) { "Error loading image from $url" }
BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB)
}
}
}

private fun downloadImage(url: String): BufferedImage {
try {
val connection = URL(url).openConnection().apply {
setRequestProperty("User-Agent", "Mozilla/5.0")
connectTimeout = 5000
readTimeout = 5000
}

return ImageIO.read(connection.inputStream)
} catch (e: Exception) {
logger.error(e) { "Error downloading image from $url" }
return BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB)
}
}


fun Graphics2D.drawTextWithFont(width: Int, height: Int, textConfig: TextConfig.() -> Unit) {
val config = TextConfig().apply(textConfig)
this.font = getFont(config.fontFamily, config.fontSize) ?: Font("SansSerif", Font.PLAIN, config.fontSize)
this.color = color
this.drawString(config.text, (width / config.textPosition.x), (height / config.textPosition.y))
}

data class TextConfig(
var text: String = "",
var fontSize: Int = 16,
var fontFamily: String = "SansSerif",
var fontColor: Color = java.awt.Color.WHITE,
var textPosition: ImagePosition = ImagePosition(0f, 0f, null)
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import net.cakeyfox.common.Constants
import net.cakeyfox.common.FoxyEmotes
import net.cakeyfox.foxy.command.FoxyInteractionContext
import net.cakeyfox.foxy.utils.pretty
import net.cakeyfox.foxy.utils.profile.badge.BadgeCondition
import net.cakeyfox.serializable.data.ImagePosition
import net.cakeyfox.serializable.database.data.*
import net.dv8tion.jda.api.entities.Member
import net.dv8tion.jda.api.entities.User
Expand All @@ -28,25 +30,21 @@ import java.time.Instant
import javax.imageio.ImageIO
import kotlin.reflect.jvm.jvmName

// TODO: Refactor this
class FoxyProfileRender(
val context: FoxyInteractionContext
) {
companion object {
val backgroundCache: Cache<String, Background> = Caffeine.newBuilder()
.build()

val layoutCache: Cache<String, Layout> = Caffeine.newBuilder()
.build()

val badgeCache: Cache<String, List<Badge>> = Caffeine.newBuilder()
.build()
val backgroundCache: Cache<String, Background> = Caffeine.newBuilder().build()
val layoutCache: Cache<String, Layout> = Caffeine.newBuilder().build()
val badgeCache: Cache<String, List<Badge>> = Caffeine.newBuilder().build()

private val logger = KotlinLogging.logger(this::class.jvmName)
private const val width = 1436
private const val height = 884
private const val PROFILE_WIDTH = 1436
private const val PROFILE_HEIGHT = 884
}

private var image: BufferedImage = BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB)
private var image: BufferedImage = BufferedImage(PROFILE_WIDTH, PROFILE_HEIGHT, BufferedImage.TYPE_INT_ARGB)
private var graphics: Graphics2D = image.createGraphics()

init {
Expand Down Expand Up @@ -112,10 +110,10 @@ class FoxyProfileRender(
}

private fun drawBackgroundAndLayout(background: BufferedImage, layout: BufferedImage) {
graphics.clearRect(0, 0, width, height)
graphics.drawImage(background, 0, 0, width, height, null)
graphics.drawImage(layout, 0, 0, width, height, null)
graphics.drawRect(0, 0, width, height)
graphics.clearRect(0, 0, PROFILE_WIDTH, PROFILE_HEIGHT)
graphics.drawImage(background, 0, 0, PROFILE_WIDTH, PROFILE_HEIGHT, null)
graphics.drawImage(layout, 0, 0, PROFILE_WIDTH, PROFILE_HEIGHT, null)
graphics.drawRect(0, 0, PROFILE_WIDTH, PROFILE_HEIGHT)
}

private suspend fun drawUserDetails(
Expand All @@ -136,7 +134,7 @@ class FoxyProfileRender(
if (data.marryStatus.marriedWith != null) {
val marriedDateFormatted = context.utils.convertToHumanReadableDate(data.marryStatus.marriedDate!!)
marriedCard?.let {
graphics.drawImage(it, 0, 0, width, height, null)
graphics.drawImage(it, 0, 0, PROFILE_WIDTH, PROFILE_HEIGHT, null)
drawText(context.locale["profile.marriedWith"], layoutInfo.profileSettings.fontSize.married, layoutInfo.profileSettings.defaultFont, fontColor, layoutInfo.profileSettings.positions.marriedPosition)

val partnerUser = context.jda.retrieveUserById(data.marryStatus.marriedWith!!).await()
Expand All @@ -149,10 +147,10 @@ class FoxyProfileRender(
drawUserAvatar(user, layoutInfo)
}

private fun drawText(text: String, fontSize: Int, fontFamily: String, color: Color, position: Position) {
private fun drawText(text: String, fontSize: Int, fontFamily: String, color: Color, position: ImagePosition) {
graphics.font = getFont(fontFamily, fontSize) ?: Font("SansSerif", Font.PLAIN, fontSize)
graphics.color = color
graphics.drawString(text, (width / position.x), (height / position.y))
graphics.drawString(text, (PROFILE_WIDTH / position.x), (PROFILE_HEIGHT / position.y))
}

private fun getFont(fontName: String, fontSize: Int): Font? {
Expand Down Expand Up @@ -309,7 +307,7 @@ class FoxyProfileRender(
private suspend fun drawDecoration(data: FoxyUser, layoutInfo: Layout) {
data.userProfile.decoration?.let {
val decorationImage = loadImage(Constants.PROFILE_DECORATION(it))
graphics.drawImage(decorationImage, (width / layoutInfo.profileSettings.positions.decorationPosition.x).toInt(), (height / layoutInfo.profileSettings.positions.decorationPosition.y).toInt(), 200, 200, null)
graphics.drawImage(decorationImage, (PROFILE_WIDTH / layoutInfo.profileSettings.positions.decorationPosition.x).toInt(), (PROFILE_HEIGHT / layoutInfo.profileSettings.positions.decorationPosition.y).toInt(), 200, 200, null)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package net.cakeyfox.foxy.utils.profile

import com.github.benmanes.caffeine.cache.Cache
import com.github.benmanes.caffeine.cache.Caffeine
import net.cakeyfox.foxy.utils.image.ImageUtils
import net.cakeyfox.serializable.database.data.Background
import net.cakeyfox.serializable.database.data.Badge
import net.cakeyfox.serializable.database.data.Decoration
import net.cakeyfox.serializable.database.data.Layout
import java.awt.image.BufferedImage

object ProfileCacheManager {
val backgroundCache: Cache<String, Background> = Caffeine.newBuilder().build()
val layoutCache: Cache<String, Layout> = Caffeine.newBuilder().build()
val badgesCache: Cache<String, List<Badge>> = Caffeine.newBuilder().build()
val decorationCache: Cache<String, Decoration> = Caffeine.newBuilder().build()
val imageCache: Cache<String, BufferedImage> = Caffeine.newBuilder()
.maximumSize(100)
.build()

suspend fun loadImageFromCache(url: String): BufferedImage? {
return imageCache.getIfPresent(url) ?: run {
val image = ImageUtils.loadImageFromURL(url)
imageCache.put(url, image)
image
}
}
}
Loading

0 comments on commit 3f600c4

Please sign in to comment.