Add avatar builder
This commit is contained in:
5
pom.xml
5
pom.xml
@@ -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>
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
@@ -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()
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
|
||||||
}
|
}
|
||||||
11
src/main/kotlin/top/fatweb/api/util/NumberUtil.kt
Normal file
11
src/main/kotlin/top/fatweb/api/util/NumberUtil.kt
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user