Add avatar builder

This commit is contained in:
2023-11-17 17:57:12 +08:00
parent c34d26af88
commit b81572fa51
12 changed files with 236 additions and 4 deletions

View File

@@ -169,6 +169,11 @@
<artifactId>avatar-generator</artifactId> <artifactId>avatar-generator</artifactId>
<version>1.1.0</version> <version>1.1.0</version>
</dependency> </dependency>
<dependency>
<groupId>com.talanlabs</groupId>
<artifactId>avatar-generator-8bit</artifactId>
<version>1.1.0</version>
</dependency>
</dependencies> </dependencies>
<build> <build>

View File

@@ -2,11 +2,16 @@ package top.fatweb.api.controller.api.v1
import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.tags.Tag import io.swagger.v3.oas.annotations.tags.Tag
import jakarta.validation.Valid
import org.springframework.http.MediaType
import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController import org.springframework.web.bind.annotation.RestController
import top.fatweb.api.entity.common.ResponseResult import top.fatweb.api.entity.common.ResponseResult
import top.fatweb.api.param.api.v1.avatar.AvatarBaseParam
import top.fatweb.api.param.api.v1.avatar.AvatarEightBitParam
import top.fatweb.api.param.api.v1.avatar.AvatarGitHubParam
import top.fatweb.api.service.api.v1.IAvatarService import top.fatweb.api.service.api.v1.IAvatarService
import top.fatweb.api.vo.api.v1.avatar.DefaultBase64Vo import top.fatweb.api.vo.api.v1.avatar.DefaultBase64Vo
@@ -27,4 +32,34 @@ class AvatarController(
fun getDefault(@PathVariable apiVersion: String): ResponseResult<DefaultBase64Vo> { fun getDefault(@PathVariable apiVersion: String): ResponseResult<DefaultBase64Vo> {
return ResponseResult.success(data = avatarService.getDefault()) return ResponseResult.success(data = avatarService.getDefault())
} }
@Operation(summary = "三角形头像")
@GetMapping("/triangle", produces = [MediaType.IMAGE_PNG_VALUE])
fun triangle(@PathVariable apiVersion: String, @Valid avatarBaseParam: AvatarBaseParam?): ByteArray {
return avatarService.triangle(avatarBaseParam)
}
@Operation(summary = "正方形头像")
@GetMapping("/square", produces = [MediaType.IMAGE_PNG_VALUE])
fun square(@PathVariable apiVersion: String, @Valid avatarBaseParam: AvatarBaseParam?): ByteArray {
return avatarService.square(avatarBaseParam)
}
@Operation(summary = "Identicon 头像")
@GetMapping("/identicon", produces = [MediaType.IMAGE_PNG_VALUE])
fun identicon(@PathVariable apiVersion: String, @Valid avatarBaseParam: AvatarBaseParam?): ByteArray {
return avatarService.identicon(avatarBaseParam)
}
@Operation(summary = "GitHub 头像")
@GetMapping("/github", produces = [MediaType.IMAGE_PNG_VALUE])
fun github(@PathVariable apiVersion: String, @Valid avatarGitHubParam: AvatarGitHubParam?): ByteArray {
return avatarService.github(avatarGitHubParam)
}
@Operation(summary = "8 Bit 头像")
@GetMapping("/8bit", produces = [MediaType.IMAGE_PNG_VALUE])
fun eightBit(@PathVariable apiVersion: String, @Valid avatarEightBitParam: AvatarEightBitParam): ByteArray {
return avatarService.eightBit(avatarEightBitParam)
}
} }

View File

@@ -8,5 +8,6 @@ package top.fatweb.api.entity.common
*/ */
enum class BusinessCode(val code: Int) { enum class BusinessCode(val code: Int) {
SYSTEM(100), SYSTEM(100),
DATABASE(200) DATABASE(200),
API_AVATAR(501)
} }

View File

@@ -35,7 +35,9 @@ enum class ResponseCode(val code: Int) {
DATABASE_DELETE_SUCCESS(BusinessCode.DATABASE, 30), DATABASE_DELETE_SUCCESS(BusinessCode.DATABASE, 30),
DATABASE_DELETE_FILED(BusinessCode.DATABASE, 35), DATABASE_DELETE_FILED(BusinessCode.DATABASE, 35),
DATABASE_EXECUTE_ERROR(BusinessCode.DATABASE, 40), DATABASE_EXECUTE_ERROR(BusinessCode.DATABASE, 40),
DATABASE_DUPLICATE_KEY(BusinessCode.DATABASE, 45); DATABASE_DUPLICATE_KEY(BusinessCode.DATABASE, 45),
API_AVATAR_ERROR(BusinessCode.API_AVATAR, 5);
constructor(businessCode: BusinessCode, code: Int) : this(businessCode.code * 100 + code) constructor(businessCode: BusinessCode, code: Int) : this(businessCode.code * 100 + code)
} }

View File

@@ -3,6 +3,7 @@ package top.fatweb.api.handler
import com.auth0.jwt.exceptions.JWTDecodeException import com.auth0.jwt.exceptions.JWTDecodeException
import com.auth0.jwt.exceptions.SignatureVerificationException import com.auth0.jwt.exceptions.SignatureVerificationException
import com.auth0.jwt.exceptions.TokenExpiredException import com.auth0.jwt.exceptions.TokenExpiredException
import com.talanlabs.avatargenerator.AvatarException
import org.slf4j.Logger import org.slf4j.Logger
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.dao.DuplicateKeyException import org.springframework.dao.DuplicateKeyException
@@ -88,6 +89,11 @@ class ExceptionHandler {
ResponseResult.fail(ResponseCode.DATABASE_DUPLICATE_KEY, "Duplicate key", null) ResponseResult.fail(ResponseCode.DATABASE_DUPLICATE_KEY, "Duplicate key", null)
} }
is AvatarException -> {
logger.debug(e.localizedMessage, e)
ResponseResult.fail(ResponseCode.API_AVATAR_ERROR, e.localizedMessage, null)
}
else -> { else -> {
logger.error(e.localizedMessage, e) logger.error(e.localizedMessage, e)
ResponseResult.fail(ResponseCode.SYSTEM_ERROR, e.toString(), null) ResponseResult.fail(ResponseCode.SYSTEM_ERROR, e.toString(), null)

View File

@@ -19,8 +19,8 @@ open class PageSortParam {
var pageSize: Long = 20 var pageSize: Long = 20
@Schema(description = "排序字段", example = "id") @Schema(description = "排序字段", example = "id")
val sortField: String? = null var sortField: String? = null
@Schema(description = "排序方式", example = "desc", allowableValues = ["desc", "asc"]) @Schema(description = "排序方式", example = "desc", allowableValues = ["desc", "asc"])
val sortOrder: String? = null var sortOrder: String? = null
} }

View File

@@ -0,0 +1,28 @@
package top.fatweb.api.param.api.v1.avatar
import io.swagger.v3.oas.annotations.media.Schema
import jakarta.validation.constraints.Max
import jakarta.validation.constraints.Pattern
open class AvatarBaseParam {
@Schema(description = "种子")
var seed: Long? = null
@Schema(description = "图像大小", defaultValue = "128")
@field:Max(256, message = "Size must be less than or equal to 256")
var size: Int? = null
@Schema(description = "外边距", defaultValue = "0")
var margin: Int? = null
@Schema(description = "内边距", defaultValue = "0")
var padding: Int? = null
@Schema(defaultValue = "颜色列表", example = "#FFFFFFAA")
var colors: List<String>? = null
@Schema(defaultValue = "背景颜色", example = "#FFFFFFAA")
@field:Pattern(regexp = "^#[0-9a-fA-F]{6}|#[0-9a-fA-F]{8}$", message = "Background color must be a hex color code")
var background: String? = null
}

View File

@@ -0,0 +1,8 @@
package top.fatweb.api.param.api.v1.avatar
import io.swagger.v3.oas.annotations.media.Schema
data class AvatarEightBitParam(
@Schema(description = "性别", defaultValue = "male", allowableValues = ["male", "female"])
val gender: String?,
) : AvatarBaseParam()

View File

@@ -0,0 +1,13 @@
package top.fatweb.api.param.api.v1.avatar
import io.swagger.v3.oas.annotations.media.Schema
import jakarta.validation.constraints.Max
data class AvatarGitHubParam(
@Schema(description = "元素大小", defaultValue = "400")
@field:Max(1000, message = "Element size must be less than or equal to 1000")
val elementSize: Int = 400,
@Schema(description = "精确度", defaultValue = "3")
val precision: Int = 3
) : AvatarBaseParam()

View File

@@ -1,8 +1,18 @@
package top.fatweb.api.service.api.v1 package top.fatweb.api.service.api.v1
import com.talanlabs.avatargenerator.GitHubAvatar import com.talanlabs.avatargenerator.GitHubAvatar
import com.talanlabs.avatargenerator.IdenticonAvatar
import com.talanlabs.avatargenerator.SquareAvatar
import com.talanlabs.avatargenerator.TriangleAvatar
import com.talanlabs.avatargenerator.eightbit.EightBitAvatar
import com.talanlabs.avatargenerator.layers.backgrounds.ColorPaintBackgroundLayer
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import top.fatweb.api.param.api.v1.avatar.AvatarBaseParam
import top.fatweb.api.param.api.v1.avatar.AvatarEightBitParam
import top.fatweb.api.param.api.v1.avatar.AvatarGitHubParam
import top.fatweb.api.util.NumberUtil
import top.fatweb.api.vo.api.v1.avatar.DefaultBase64Vo import top.fatweb.api.vo.api.v1.avatar.DefaultBase64Vo
import java.awt.Color
import kotlin.io.encoding.Base64 import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi import kotlin.io.encoding.ExperimentalEncodingApi
@@ -15,4 +25,103 @@ class AvatarServiceImpl : IAvatarService {
return DefaultBase64Vo(Base64.encode(bytes)) return DefaultBase64Vo(Base64.encode(bytes))
} }
override fun triangle(avatarBaseParam: AvatarBaseParam?): ByteArray {
val avatar = (
if (avatarBaseParam == null || avatarBaseParam.colors.isNullOrEmpty())
TriangleAvatar.newAvatarBuilder()
else TriangleAvatar.newAvatarBuilder(
*avatarBaseParam.colors!!.map { decodeColor(it) }.toTypedArray())
).apply {
avatarBaseParam?.size?.let { size(it, it) }
avatarBaseParam?.margin?.let { margin(it) }
avatarBaseParam?.padding?.let { padding(it) }
avatarBaseParam?.background?.let { layers(ColorPaintBackgroundLayer(decodeColor(it))) }
}.build()
return avatar.createAsPngBytes(avatarBaseParam?.seed ?: NumberUtil.getRandomLong())
}
override fun square(avatarBaseParam: AvatarBaseParam?): ByteArray {
val avatar = (
if (avatarBaseParam == null || avatarBaseParam.colors.isNullOrEmpty())
SquareAvatar.newAvatarBuilder()
else SquareAvatar.newAvatarBuilder(
*avatarBaseParam.colors!!.map { decodeColor(it) }.toTypedArray())
).apply {
avatarBaseParam?.size?.let { size(it, it) }
avatarBaseParam?.margin?.let { margin(it) }
avatarBaseParam?.padding?.let { padding(it) }
avatarBaseParam?.background?.let { layers(ColorPaintBackgroundLayer(decodeColor(it))) }
}.build()
return avatar.createAsPngBytes(avatarBaseParam?.seed ?: NumberUtil.getRandomLong())
}
override fun identicon(avatarBaseParam: AvatarBaseParam?): ByteArray {
val avatar = IdenticonAvatar.newAvatarBuilder().apply {
avatarBaseParam?.size?.let { size(it, it) }
avatarBaseParam?.margin?.let { margin(it) }
avatarBaseParam?.padding?.let { padding(it) }
if (avatarBaseParam != null && !avatarBaseParam.colors.isNullOrEmpty()) {
color(decodeColor(avatarBaseParam.colors!!.random()))
}
avatarBaseParam?.background?.let { layers(ColorPaintBackgroundLayer(decodeColor(it))) }
}.build()
return avatar.createAsPngBytes(avatarBaseParam?.seed ?: NumberUtil.getRandomLong())
}
override fun github(avatarGitHubParam: AvatarGitHubParam?): ByteArray {
val avatar = (avatarGitHubParam?.let { GitHubAvatar.newAvatarBuilder(it.elementSize, it.precision) }
?: let { GitHubAvatar.newAvatarBuilder() }).apply {
avatarGitHubParam?.size?.let { size(it, it) }
avatarGitHubParam?.margin?.let { margin(it) }
avatarGitHubParam?.padding?.let { padding(it) }
if (avatarGitHubParam != null && !avatarGitHubParam.colors.isNullOrEmpty()) {
color(decodeColor(avatarGitHubParam.colors!!.random()))
}
avatarGitHubParam?.background?.let { layers(ColorPaintBackgroundLayer(decodeColor(it))) }
}.build()
return avatar.createAsPngBytes(avatarGitHubParam?.seed ?: NumberUtil.getRandomLong())
}
override fun eightBit(avatarEightBitParam: AvatarEightBitParam?): ByteArray {
val avatar = if (avatarEightBitParam?.gender?.equals("female") ?: false) {
EightBitAvatar.newFemaleAvatarBuilder().apply {
avatarEightBitParam?.size?.let { size(it, it) }
avatarEightBitParam?.margin?.let { margin(it) }
avatarEightBitParam?.padding?.let { padding(it) }
if (avatarEightBitParam != null && !avatarEightBitParam.colors.isNullOrEmpty()) {
color(decodeColor(avatarEightBitParam.colors!!.random()))
}
avatarEightBitParam?.background?.let { layers(ColorPaintBackgroundLayer(decodeColor(it))) }
}.build()
} else {
EightBitAvatar.newMaleAvatarBuilder().apply {
avatarEightBitParam?.size?.let { size(it, it) }
avatarEightBitParam?.margin?.let { margin(it) }
avatarEightBitParam?.padding?.let { padding(it) }
if (avatarEightBitParam != null && !avatarEightBitParam.colors.isNullOrEmpty()) {
color(decodeColor(avatarEightBitParam.colors!!.random()))
}
avatarEightBitParam?.background?.let { layers(ColorPaintBackgroundLayer(decodeColor(it))) }
}.build()
}
return avatar.createAsPngBytes(avatarEightBitParam?.seed ?: NumberUtil.getRandomLong())
}
fun decodeColor(nm: String): Color {
return if (Regex("^#[0-9a-fA-F]{6}$").matches(nm)) {
Color.decode(nm)
} else if (Regex("^#[0-9a-fA-F]{8}$").matches(nm)) {
val intVal = Integer.decode(nm.substring(1..6).prependIndent("#"))
val alpha = Integer.decode(nm.substring(7).prependIndent("#"))
Color(intVal shr 16 and 0xFF, intVal shr 8 and 0XFF, intVal and 0xFF, alpha and 0xFF)
} else {
Color.WHITE
}
}
} }

View File

@@ -1,7 +1,21 @@
package top.fatweb.api.service.api.v1 package top.fatweb.api.service.api.v1
import top.fatweb.api.param.api.v1.avatar.AvatarBaseParam
import top.fatweb.api.param.api.v1.avatar.AvatarEightBitParam
import top.fatweb.api.param.api.v1.avatar.AvatarGitHubParam
import top.fatweb.api.vo.api.v1.avatar.DefaultBase64Vo import top.fatweb.api.vo.api.v1.avatar.DefaultBase64Vo
interface IAvatarService { interface IAvatarService {
fun getDefault(): DefaultBase64Vo fun getDefault(): DefaultBase64Vo
fun triangle(avatarBaseParam: AvatarBaseParam?): ByteArray
fun square(avatarBaseParam: AvatarBaseParam?): ByteArray
fun identicon(avatarBaseParam: AvatarBaseParam?): ByteArray
fun github(avatarGitHubParam: AvatarGitHubParam?): ByteArray
fun eightBit(avatarEightBitParam: AvatarEightBitParam?): ByteArray
} }

View File

@@ -0,0 +1,11 @@
package top.fatweb.api.util
object NumberUtil {
fun getRandomInt(start: Int = Int.MIN_VALUE, end: Int = Int.MAX_VALUE): Int {
return (start..end).random()
}
fun getRandomLong(start: Long = Long.MIN_VALUE, end: Long = Long.MAX_VALUE): Long {
return (start..end).random()
}
}