diff --git a/pom.xml b/pom.xml index 28fe6b6..62840b6 100644 --- a/pom.xml +++ b/pom.xml @@ -169,6 +169,11 @@ avatar-generator 1.1.0 + + com.talanlabs + avatar-generator-8bit + 1.1.0 + diff --git a/src/main/kotlin/top/fatweb/api/controller/api/v1/AvatarController.kt b/src/main/kotlin/top/fatweb/api/controller/api/v1/AvatarController.kt index c8d0808..855f631 100644 --- a/src/main/kotlin/top/fatweb/api/controller/api/v1/AvatarController.kt +++ b/src/main/kotlin/top/fatweb/api/controller/api/v1/AvatarController.kt @@ -2,11 +2,16 @@ package top.fatweb.api.controller.api.v1 import io.swagger.v3.oas.annotations.Operation 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.PathVariable import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController 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.vo.api.v1.avatar.DefaultBase64Vo @@ -27,4 +32,34 @@ class AvatarController( fun getDefault(@PathVariable apiVersion: String): ResponseResult { 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) + } } \ No newline at end of file diff --git a/src/main/kotlin/top/fatweb/api/entity/common/BusinessCode.kt b/src/main/kotlin/top/fatweb/api/entity/common/BusinessCode.kt index e4bc399..046ddd8 100644 --- a/src/main/kotlin/top/fatweb/api/entity/common/BusinessCode.kt +++ b/src/main/kotlin/top/fatweb/api/entity/common/BusinessCode.kt @@ -8,5 +8,6 @@ package top.fatweb.api.entity.common */ enum class BusinessCode(val code: Int) { SYSTEM(100), - DATABASE(200) + DATABASE(200), + API_AVATAR(501) } \ No newline at end of file diff --git a/src/main/kotlin/top/fatweb/api/entity/common/ResponseCode.kt b/src/main/kotlin/top/fatweb/api/entity/common/ResponseCode.kt index 4de6865..ff6ec9f 100644 --- a/src/main/kotlin/top/fatweb/api/entity/common/ResponseCode.kt +++ b/src/main/kotlin/top/fatweb/api/entity/common/ResponseCode.kt @@ -35,7 +35,9 @@ enum class ResponseCode(val code: Int) { DATABASE_DELETE_SUCCESS(BusinessCode.DATABASE, 30), DATABASE_DELETE_FILED(BusinessCode.DATABASE, 35), 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) } \ No newline at end of file diff --git a/src/main/kotlin/top/fatweb/api/handler/ExceptionHandler.kt b/src/main/kotlin/top/fatweb/api/handler/ExceptionHandler.kt index a1d3100..f3d284f 100644 --- a/src/main/kotlin/top/fatweb/api/handler/ExceptionHandler.kt +++ b/src/main/kotlin/top/fatweb/api/handler/ExceptionHandler.kt @@ -3,6 +3,7 @@ package top.fatweb.api.handler import com.auth0.jwt.exceptions.JWTDecodeException import com.auth0.jwt.exceptions.SignatureVerificationException import com.auth0.jwt.exceptions.TokenExpiredException +import com.talanlabs.avatargenerator.AvatarException import org.slf4j.Logger import org.slf4j.LoggerFactory import org.springframework.dao.DuplicateKeyException @@ -88,6 +89,11 @@ class ExceptionHandler { 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 -> { logger.error(e.localizedMessage, e) ResponseResult.fail(ResponseCode.SYSTEM_ERROR, e.toString(), null) diff --git a/src/main/kotlin/top/fatweb/api/param/PageSortParam.kt b/src/main/kotlin/top/fatweb/api/param/PageSortParam.kt index a90d411..0a38ac1 100644 --- a/src/main/kotlin/top/fatweb/api/param/PageSortParam.kt +++ b/src/main/kotlin/top/fatweb/api/param/PageSortParam.kt @@ -19,8 +19,8 @@ open class PageSortParam { var pageSize: Long = 20 @Schema(description = "排序字段", example = "id") - val sortField: String? = null + var sortField: String? = null @Schema(description = "排序方式", example = "desc", allowableValues = ["desc", "asc"]) - val sortOrder: String? = null + var sortOrder: String? = null } \ No newline at end of file diff --git a/src/main/kotlin/top/fatweb/api/param/api/v1/avatar/AvatarBaseParam.kt b/src/main/kotlin/top/fatweb/api/param/api/v1/avatar/AvatarBaseParam.kt new file mode 100644 index 0000000..669db14 --- /dev/null +++ b/src/main/kotlin/top/fatweb/api/param/api/v1/avatar/AvatarBaseParam.kt @@ -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? = 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 + +} diff --git a/src/main/kotlin/top/fatweb/api/param/api/v1/avatar/AvatarEightBitParam.kt b/src/main/kotlin/top/fatweb/api/param/api/v1/avatar/AvatarEightBitParam.kt new file mode 100644 index 0000000..f47178b --- /dev/null +++ b/src/main/kotlin/top/fatweb/api/param/api/v1/avatar/AvatarEightBitParam.kt @@ -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() diff --git a/src/main/kotlin/top/fatweb/api/param/api/v1/avatar/AvatarGitHubParam.kt b/src/main/kotlin/top/fatweb/api/param/api/v1/avatar/AvatarGitHubParam.kt new file mode 100644 index 0000000..9c74c1c --- /dev/null +++ b/src/main/kotlin/top/fatweb/api/param/api/v1/avatar/AvatarGitHubParam.kt @@ -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() diff --git a/src/main/kotlin/top/fatweb/api/service/api/v1/AvatarServiceImpl.kt b/src/main/kotlin/top/fatweb/api/service/api/v1/AvatarServiceImpl.kt index 24ce514..57a0e6e 100644 --- a/src/main/kotlin/top/fatweb/api/service/api/v1/AvatarServiceImpl.kt +++ b/src/main/kotlin/top/fatweb/api/service/api/v1/AvatarServiceImpl.kt @@ -1,8 +1,18 @@ package top.fatweb.api.service.api.v1 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 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 java.awt.Color import kotlin.io.encoding.Base64 import kotlin.io.encoding.ExperimentalEncodingApi @@ -15,4 +25,103 @@ class AvatarServiceImpl : IAvatarService { 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 + } + } + } \ No newline at end of file diff --git a/src/main/kotlin/top/fatweb/api/service/api/v1/IAvatarService.kt b/src/main/kotlin/top/fatweb/api/service/api/v1/IAvatarService.kt index c07bd20..5737b79 100644 --- a/src/main/kotlin/top/fatweb/api/service/api/v1/IAvatarService.kt +++ b/src/main/kotlin/top/fatweb/api/service/api/v1/IAvatarService.kt @@ -1,7 +1,21 @@ 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 interface IAvatarService { 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 + } \ No newline at end of file diff --git a/src/main/kotlin/top/fatweb/api/util/NumberUtil.kt b/src/main/kotlin/top/fatweb/api/util/NumberUtil.kt new file mode 100644 index 0000000..ebc074b --- /dev/null +++ b/src/main/kotlin/top/fatweb/api/util/NumberUtil.kt @@ -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() + } +} \ No newline at end of file