mirror of
https://github.com/gotson/komga.git
synced 2026-05-09 05:10:19 +02:00
parent
68467de034
commit
9b11e47ed2
6 changed files with 121 additions and 20 deletions
|
|
@ -67,6 +67,7 @@ dependencies {
|
|||
|
||||
implementation("net.coobird:thumbnailator:0.4.8")
|
||||
implementation("com.twelvemonkeys.imageio:imageio-jpeg:3.4.2")
|
||||
implementation("com.twelvemonkeys.imageio:imageio-tiff:3.4.2")
|
||||
|
||||
runtimeOnly("com.h2database:h2:1.4.199")
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
package org.gotson.komga.domain.model
|
||||
|
||||
class BookPageContent(
|
||||
val number: Int,
|
||||
val content: ByteArray,
|
||||
val mediaType: String
|
||||
)
|
||||
|
|
@ -4,8 +4,14 @@ import mu.KotlinLogging
|
|||
import org.apache.commons.lang3.time.DurationFormatUtils
|
||||
import org.gotson.komga.domain.model.Book
|
||||
import org.gotson.komga.domain.model.BookMetadata
|
||||
import org.gotson.komga.domain.model.BookPageContent
|
||||
import org.gotson.komga.domain.model.Status
|
||||
import org.gotson.komga.domain.persistence.BookRepository
|
||||
import org.gotson.komga.infrastructure.image.ImageConverter
|
||||
import org.gotson.komga.infrastructure.image.ImageType
|
||||
import org.gotson.komga.infrastructure.image.mediaTypeToImageIOFormat
|
||||
import org.gotson.komga.infrastructure.image.toImageIOFormat
|
||||
import org.gotson.komga.infrastructure.image.toMediaType
|
||||
import org.springframework.scheduling.annotation.Async
|
||||
import org.springframework.scheduling.annotation.AsyncResult
|
||||
import org.springframework.stereotype.Service
|
||||
|
|
@ -18,7 +24,8 @@ private val logger = KotlinLogging.logger {}
|
|||
@Service
|
||||
class BookManager(
|
||||
private val bookRepository: BookRepository,
|
||||
private val bookParser: BookParser
|
||||
private val bookParser: BookParser,
|
||||
private val imageConverter: ImageConverter
|
||||
) {
|
||||
|
||||
@Transactional
|
||||
|
|
@ -53,4 +60,31 @@ class BookManager(
|
|||
bookRepository.save(book)
|
||||
}.also { logger.info { "Thumbnail generated in ${DurationFormatUtils.formatDurationHMS(it)}" } })
|
||||
}
|
||||
|
||||
fun getBookPage(book: Book, number: Int, convertTo: ImageType = ImageType.ORIGINAL): BookPageContent {
|
||||
val pageContent = bookParser.getPageContent(book, number)
|
||||
val pageMediaType = book.metadata.pages[number - 1].mediaType
|
||||
|
||||
if (convertTo != ImageType.ORIGINAL) {
|
||||
val pageFormat = mediaTypeToImageIOFormat(pageMediaType)
|
||||
val convertFormat = convertTo.toImageIOFormat()
|
||||
if (pageFormat != null && convertFormat != null && imageConverter.canConvert(pageFormat, convertFormat)) {
|
||||
if (pageFormat != convertFormat) {
|
||||
try {
|
||||
logger.info { "Trying to convert page #$number of book $book from $pageFormat to $convertFormat" }
|
||||
val convertedPage = imageConverter.convertImage(pageContent, convertFormat)
|
||||
return BookPageContent(number, convertedPage, convertTo.toMediaType() ?: "application/octet-stream")
|
||||
} catch (ex: Exception) {
|
||||
logger.error(ex) { "Failed to convert page #$number of book $book to $convertFormat" }
|
||||
}
|
||||
} else {
|
||||
logger.warn { "Cannot convert page #$number of book $book from $pageFormat to $convertFormat: same format" }
|
||||
}
|
||||
} else {
|
||||
logger.warn { "Cannot convert page #$number of book $book to $convertFormat: unsupported format" }
|
||||
}
|
||||
}
|
||||
|
||||
return BookPageContent(number, pageContent, pageMediaType)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package org.gotson.komga.infrastructure.image
|
||||
|
||||
import mu.KotlinLogging
|
||||
import org.springframework.stereotype.Service
|
||||
import java.io.ByteArrayOutputStream
|
||||
import javax.imageio.ImageIO
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
@Service
|
||||
class ImageConverter {
|
||||
|
||||
val supportedReadFormats = ImageIO.getReaderFormatNames().toList()
|
||||
val supportedWriteFormats = ImageIO.getWriterFormatNames().toList()
|
||||
|
||||
init {
|
||||
logger.info { "Supported read formats: $supportedReadFormats" }
|
||||
logger.info { "Supported write formats: $supportedWriteFormats" }
|
||||
}
|
||||
|
||||
fun canConvert(from: String, to: String) =
|
||||
supportedReadFormats.contains(from) && supportedWriteFormats.contains(to)
|
||||
|
||||
fun convertImage(imageBytes: ByteArray, format: String): ByteArray =
|
||||
ByteArrayOutputStream().use {
|
||||
val image = ImageIO.read(imageBytes.inputStream())
|
||||
ImageIO.write(image, format, it)
|
||||
it.toByteArray()
|
||||
}
|
||||
}
|
||||
|
||||
fun mediaTypeToImageIOFormat(mediaType: String): String? =
|
||||
if (mediaType.startsWith("image/", ignoreCase = true))
|
||||
mediaType.toLowerCase().substringAfter("/")
|
||||
else
|
||||
null
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package org.gotson.komga.infrastructure.image
|
||||
|
||||
enum class ImageType {
|
||||
ORIGINAL, PNG, JPEG
|
||||
}
|
||||
|
||||
fun ImageType.toMediaType(): String? =
|
||||
when (this) {
|
||||
ImageType.ORIGINAL -> null
|
||||
ImageType.PNG -> "image/png"
|
||||
ImageType.JPEG -> "image/jpeg"
|
||||
}
|
||||
|
||||
fun ImageType.toImageIOFormat(): String? =
|
||||
when (this) {
|
||||
ImageType.ORIGINAL -> null
|
||||
ImageType.PNG -> "png"
|
||||
ImageType.JPEG -> "jpeg"
|
||||
}
|
||||
|
|
@ -9,8 +9,9 @@ import org.gotson.komga.domain.model.Serie
|
|||
import org.gotson.komga.domain.model.Status
|
||||
import org.gotson.komga.domain.persistence.BookRepository
|
||||
import org.gotson.komga.domain.persistence.SerieRepository
|
||||
import org.gotson.komga.domain.service.BookParser
|
||||
import org.gotson.komga.domain.service.BookManager
|
||||
import org.gotson.komga.domain.service.MetadataNotReadyException
|
||||
import org.gotson.komga.infrastructure.image.ImageType
|
||||
import org.springframework.data.domain.Page
|
||||
import org.springframework.data.domain.PageRequest
|
||||
import org.springframework.data.domain.Pageable
|
||||
|
|
@ -41,7 +42,7 @@ private val logger = KotlinLogging.logger {}
|
|||
class SerieController(
|
||||
private val serieRepository: SerieRepository,
|
||||
private val bookRepository: BookRepository,
|
||||
private val bookParser: BookParser
|
||||
private val bookManager: BookManager
|
||||
) {
|
||||
|
||||
@GetMapping
|
||||
|
|
@ -134,13 +135,6 @@ class SerieController(
|
|||
): ResponseEntity<ByteArray> {
|
||||
if (!serieRepository.existsById(serieId)) throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
return bookRepository.findByIdOrNull(bookId)?.let { book ->
|
||||
|
||||
val mediaType = try {
|
||||
MediaType.parseMediaType(book.metadata.mediaType!!)
|
||||
} catch (ex: Exception) {
|
||||
MediaType.APPLICATION_OCTET_STREAM
|
||||
}
|
||||
|
||||
try {
|
||||
ResponseEntity.ok()
|
||||
.headers(HttpHeaders().apply {
|
||||
|
|
@ -148,7 +142,7 @@ class SerieController(
|
|||
.filename(FilenameUtils.getName(book.url.toString()))
|
||||
.build()
|
||||
})
|
||||
.contentType(mediaType)
|
||||
.contentType(getMediaTypeOrDefault(book.metadata.mediaType))
|
||||
.body(File(book.url.toURI()).readBytes())
|
||||
} catch (ex: FileNotFoundException) {
|
||||
logger.warn(ex) { "File not found: $book" }
|
||||
|
|
@ -176,23 +170,23 @@ class SerieController(
|
|||
fun getBookPage(
|
||||
@PathVariable serieId: Long,
|
||||
@PathVariable bookId: Long,
|
||||
@PathVariable pageNumber: Int
|
||||
@PathVariable pageNumber: Int,
|
||||
@RequestParam(value = "convert") convertTo: String?
|
||||
): ResponseEntity<ByteArray> {
|
||||
if (!serieRepository.existsById(serieId)) throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
|
||||
return bookRepository.findByIdOrNull((bookId))?.let { book ->
|
||||
try {
|
||||
val pageContent = bookParser.getPageContent(book, pageNumber)
|
||||
|
||||
val mediaType = try {
|
||||
MediaType.parseMediaType(book.metadata.pages[pageNumber - 1].mediaType)
|
||||
} catch (ex: Exception) {
|
||||
MediaType.APPLICATION_OCTET_STREAM
|
||||
val convertFormat = when (convertTo?.toLowerCase()) {
|
||||
"jpg", "jpeg" -> ImageType.JPEG
|
||||
"png" -> ImageType.PNG
|
||||
else -> ImageType.ORIGINAL
|
||||
}
|
||||
val pageContent = bookManager.getBookPage(book, pageNumber, convertFormat)
|
||||
|
||||
ResponseEntity.ok()
|
||||
.contentType(mediaType)
|
||||
.body(pageContent)
|
||||
.contentType(getMediaTypeOrDefault(pageContent.mediaType))
|
||||
.body(pageContent.content)
|
||||
} catch (ex: ArrayIndexOutOfBoundsException) {
|
||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Page number does not exist")
|
||||
} catch (ex: MetadataNotReadyException) {
|
||||
|
|
@ -203,6 +197,16 @@ class SerieController(
|
|||
}
|
||||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
}
|
||||
|
||||
private fun getMediaTypeOrDefault(mediaTypeString: String?): MediaType {
|
||||
mediaTypeString?.let {
|
||||
try {
|
||||
return MediaType.parseMediaType(mediaTypeString)
|
||||
} catch (ex: Exception) {
|
||||
}
|
||||
}
|
||||
return MediaType.APPLICATION_OCTET_STREAM
|
||||
}
|
||||
}
|
||||
|
||||
data class SerieDto(
|
||||
|
|
|
|||
Loading…
Reference in a new issue