Skip to content

Commit

Permalink
feat(api): add thumbnail size server setting
Browse files Browse the repository at this point in the history
Closes: #1031
Closes: #861
  • Loading branch information
gotson committed Oct 9, 2023
1 parent e35d468 commit 5fa789b
Show file tree
Hide file tree
Showing 12 changed files with 85 additions and 22 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package org.gotson.komga.domain.model

enum class ThumbnailSize(val maxEdge: Int) {
DEFAULT(300),
MEDIUM(600),
LARGE(900),
XLARGE(1200),
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import org.gotson.komga.domain.model.Media
import org.gotson.komga.domain.model.MediaNotReadyException
import org.gotson.komga.domain.model.MediaUnsupportedException
import org.gotson.komga.domain.model.ThumbnailBook
import org.gotson.komga.infrastructure.configuration.KomgaSettingsProvider
import org.gotson.komga.infrastructure.hash.Hasher
import org.gotson.komga.infrastructure.image.ImageAnalyzer
import org.gotson.komga.infrastructure.image.ImageConverter
Expand All @@ -35,14 +36,14 @@ class BookAnalyzer(
private val imageAnalyzer: ImageAnalyzer,
private val hasher: Hasher,
@Value("#{@komgaProperties.pageHashing}") private val pageHashing: Int,
private val komgaSettingsProvider: KomgaSettingsProvider,
) {

val supportedMediaTypes = extractors
.flatMap { e -> e.mediaTypes().map { it to e } }
.toMap()

private val thumbnailSize = 300
private val thumbnailFormat = "jpeg"
val thumbnailType = ImageType.JPEG

fun analyze(book: Book, analyzeDimensions: Boolean): Media {
logger.info { "Trying to analyze book: $book" }
Expand Down Expand Up @@ -122,7 +123,7 @@ class BookAnalyzer(
if (coverBytes == null) coverBytes = extractor.getEntryStream(book.book.path, book.media.pages.first().fileName)

coverBytes.let { cover ->
imageConverter.resizeImage(cover, thumbnailFormat, thumbnailSize)
imageConverter.resizeImage(cover, thumbnailType, komgaSettingsProvider.thumbnailSize.maxEdge)
}
} catch (ex: Exception) {
logger.warn(ex) { "Could not generate thumbnail for book: $book" }
Expand All @@ -133,7 +134,7 @@ class BookAnalyzer(
thumbnail = thumbnail,
type = ThumbnailBook.Type.GENERATED,
bookId = book.book.id,
mediaType = "image/$thumbnailFormat",
mediaType = thumbnailType.mediaType,
dimension = thumbnail?.let { imageAnalyzer.getDimension(it.inputStream()) } ?: Dimension(0, 0),
fileSize = thumbnail?.size?.toLong() ?: 0,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ class BookLifecycle(

if (resizeTo != null) {
return try {
imageConverter.resizeImage(thumbnailBytes, resizeTargetFormat.imageIOFormat, resizeTo)
imageConverter.resizeImage(thumbnailBytes, resizeTargetFormat, resizeTo)
} catch (e: Exception) {
logger.error(e) { "Resize thumbnail of book $bookId to $resizeTo: failed" }
thumbnailBytes
Expand Down Expand Up @@ -244,7 +244,7 @@ class BookLifecycle(

if (resizeTo != null) {
val convertedPage = try {
imageConverter.resizeImage(pageContent, resizeTargetFormat.imageIOFormat, resizeTo)
imageConverter.resizeImage(pageContent, resizeTargetFormat, resizeTo)
} catch (e: Exception) {
logger.error(e) { "Resize page #$number of book $book to $resizeTo: failed" }
throw e
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.gotson.komga.infrastructure.configuration

import org.apache.commons.lang3.RandomStringUtils
import org.gotson.komga.domain.model.ThumbnailSize
import org.gotson.komga.infrastructure.jooq.ServerSettingsDao
import org.springframework.stereotype.Service
import kotlin.time.Duration
Expand Down Expand Up @@ -44,11 +45,21 @@ class KomgaSettingsProvider(
serverSettingsDao.saveSetting(Settings.REMEMBER_ME_DURATION.name, value.inWholeDays.toInt())
field = value
}

var thumbnailSize: ThumbnailSize =
serverSettingsDao.getSettingByKey(Settings.THUMBNAIL_SIZE.name, String::class.java)?.let {
ThumbnailSize.valueOf(it)
} ?: ThumbnailSize.DEFAULT
set(value) {
serverSettingsDao.saveSetting(Settings.THUMBNAIL_SIZE.name, value.name)
field = value
}
}

private enum class Settings {
DELETE_EMPTY_COLLECTIONS,
DELETE_EMPTY_READLISTS,
REMEMBER_ME_KEY,
REMEMBER_ME_DURATION,
THUMBNAIL_SIZE,
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import java.awt.Color
import java.awt.image.BufferedImage
import java.io.ByteArrayOutputStream
import javax.imageio.ImageIO
import kotlin.math.min

private val logger = KotlinLogging.logger {}

Expand Down Expand Up @@ -49,15 +50,16 @@ class ImageConverter {
baos.toByteArray()
}

fun resizeImage(imageBytes: ByteArray, format: String, size: Int): ByteArray =
ByteArrayOutputStream().use {
fun resizeImage(imageBytes: ByteArray, format: ImageType, size: Int): ByteArray {
return ByteArrayOutputStream().use {
Thumbnails.of(imageBytes.inputStream())
.size(size, size)
.imageType(BufferedImage.TYPE_INT_ARGB)
.outputFormat(format)
.outputFormat(format.imageIOFormat)
.toOutputStream(it)
it.toByteArray()
}
}

private fun containsAlphaChannel(image: BufferedImage): Boolean =
image.colorModel.hasAlpha()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package org.gotson.komga.infrastructure.image

enum class ImageType(val mediaType: String, val imageIOFormat: String) {
PNG("image/png", "png"),
JPEG("image/jpeg", "jpeg"),
PNG("image/png", "PNG"),
JPEG("image/jpeg", "JPEG"),
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import org.gotson.komga.domain.persistence.ReferentialRepository
import org.gotson.komga.domain.persistence.SeriesCollectionRepository
import org.gotson.komga.domain.persistence.SeriesMetadataRepository
import org.gotson.komga.domain.service.BookLifecycle
import org.gotson.komga.infrastructure.configuration.KomgaSettingsProvider
import org.gotson.komga.infrastructure.jooq.toCurrentTimeZone
import org.gotson.komga.infrastructure.security.KomgaPrincipal
import org.gotson.komga.infrastructure.swagger.PageAsQueryParam
Expand Down Expand Up @@ -107,6 +108,7 @@ class OpdsController(
private val referentialRepository: ReferentialRepository,
private val bookRepository: BookRepository,
private val bookLifecycle: BookLifecycle,
private val komgaSettingsProvider: KomgaSettingsProvider,
) {

private val komgaAuthor = OpdsAuthor("Komga", URI("https://github.com/gotson/komga"))
Expand Down Expand Up @@ -663,7 +665,7 @@ class OpdsController(
principal.user.checkContentRestriction(bookId, bookRepository, seriesMetadataRepository)
val thumbnail = bookLifecycle.getThumbnail(bookId) ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)

return bookLifecycle.getThumbnailBytes(bookId, if (thumbnail.type == ThumbnailBook.Type.GENERATED) null else 300)
return bookLifecycle.getThumbnailBytes(bookId, if (thumbnail.type == ThumbnailBook.Type.GENERATED) null else komgaSettingsProvider.thumbnailSize.maxEdge)
?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import org.gotson.komga.domain.model.ROLE_ADMIN
import org.gotson.komga.infrastructure.configuration.KomgaSettingsProvider
import org.gotson.komga.interfaces.api.rest.dto.SettingsDto
import org.gotson.komga.interfaces.api.rest.dto.SettingsUpdateDto
import org.gotson.komga.interfaces.api.rest.dto.toDomain
import org.gotson.komga.interfaces.api.rest.dto.toDto
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.security.access.prepost.PreAuthorize
Expand All @@ -29,6 +31,7 @@ class SettingsController(
komgaSettingsProvider.deleteEmptyCollections,
komgaSettingsProvider.deleteEmptyReadLists,
komgaSettingsProvider.rememberMeDuration.inWholeDays,
komgaSettingsProvider.thumbnailSize.toDto(),
)

@PatchMapping
Expand All @@ -41,5 +44,6 @@ class SettingsController(
newSettings.deleteEmptyReadLists?.let { komgaSettingsProvider.deleteEmptyReadLists = it }
newSettings.rememberMeDurationDays?.let { komgaSettingsProvider.rememberMeDuration = it.days }
if (newSettings.renewRememberMeKey == true) komgaSettingsProvider.renewRememberMeKey()
newSettings.thumbnailSize?.let { komgaSettingsProvider.thumbnailSize = it.toDomain() }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ data class SettingsDto(
val deleteEmptyCollections: Boolean,
val deleteEmptyReadLists: Boolean,
val rememberMeDurationDays: Long,
val thumbnailSize: ThumbnailSizeDto,
)
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ data class SettingsUpdateDto(
@get:Positive
val rememberMeDurationDays: Long? = null,
val renewRememberMeKey: Boolean? = null,
val thumbnailSize: ThumbnailSizeDto? = null,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package org.gotson.komga.interfaces.api.rest.dto

import org.gotson.komga.domain.model.ThumbnailSize

enum class ThumbnailSizeDto {
DEFAULT,
MEDIUM,
LARGE,
XLARGE,
}

fun ThumbnailSize.toDto() = when (this) {
ThumbnailSize.DEFAULT -> ThumbnailSizeDto.DEFAULT
ThumbnailSize.MEDIUM -> ThumbnailSizeDto.MEDIUM
ThumbnailSize.LARGE -> ThumbnailSizeDto.LARGE
ThumbnailSize.XLARGE -> ThumbnailSizeDto.XLARGE
}

fun ThumbnailSizeDto.toDomain() = when (this) {
ThumbnailSizeDto.DEFAULT -> ThumbnailSize.DEFAULT
ThumbnailSizeDto.MEDIUM -> ThumbnailSize.MEDIUM
ThumbnailSizeDto.LARGE -> ThumbnailSize.LARGE
ThumbnailSizeDto.XLARGE -> ThumbnailSize.XLARGE
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ package org.gotson.komga.interfaces.api.rest
import org.assertj.core.api.Assertions.assertThat
import org.gotson.komga.domain.model.ROLE_ADMIN
import org.gotson.komga.domain.model.ROLE_USER
import org.gotson.komga.domain.model.ThumbnailSize
import org.gotson.komga.infrastructure.configuration.KomgaSettingsProvider
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.ValueSource
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
Expand Down Expand Up @@ -49,13 +52,15 @@ class SettingsControllerTest(
komgaSettingsProvider.deleteEmptyCollections = true
komgaSettingsProvider.deleteEmptyReadLists = false
komgaSettingsProvider.rememberMeDuration = 5.days
komgaSettingsProvider.thumbnailSize = ThumbnailSize.LARGE

mockMvc.get("/api/v1/settings")
.andExpect {
status { isOk() }
jsonPath("deleteEmptyCollections") { value(true) }
jsonPath("deleteEmptyReadLists") { value(false) }
jsonPath("rememberMeDurationDays") { value(5) }
jsonPath("thumbnailSize") { value("LARGE") }
}
}

Expand All @@ -65,6 +70,7 @@ class SettingsControllerTest(
komgaSettingsProvider.deleteEmptyCollections = true
komgaSettingsProvider.deleteEmptyReadLists = true
komgaSettingsProvider.rememberMeDuration = 5.days
komgaSettingsProvider.thumbnailSize = ThumbnailSize.LARGE

val rememberMeKey = komgaSettingsProvider.rememberMeKey

Expand All @@ -73,7 +79,8 @@ class SettingsControllerTest(
{
"deleteEmptyCollections": false,
"rememberMeDurationDays": 15,
"renewRememberMeKey": true
"renewRememberMeKey": true,
"thumbnailSize": "MEDIUM"
}
""".trimIndent()

Expand All @@ -89,18 +96,20 @@ class SettingsControllerTest(
assertThat(komgaSettingsProvider.deleteEmptyReadLists).isTrue
assertThat(komgaSettingsProvider.rememberMeDuration).isEqualTo(15.days)
assertThat(komgaSettingsProvider.rememberMeKey).isNotEqualTo(rememberMeKey)
assertThat(komgaSettingsProvider.thumbnailSize).isEqualTo(ThumbnailSize.MEDIUM)
}

@Test
@ParameterizedTest
@WithMockCustomUser(roles = [ROLE_ADMIN])
fun `given admin user when updating with invalid settings then returns bad request`() {
//language=JSON
val jsonString = """
{
"rememberMeDurationDays": 0
}
""".trimIndent()

@ValueSource(
strings = [
//language=JSON
"""{"rememberMeDurationDays": 0}""",
//language=JSON
"""{"thumbnailSize": "HUGE"}""",
],
)
fun `given admin user when updating with invalid settings then returns bad request`(jsonString: String) {
mockMvc.patch("/api/v1/settings") {
contentType = MediaType.APPLICATION_JSON
content = jsonString
Expand Down

0 comments on commit 5fa789b

Please sign in to comment.