mirror of
https://github.com/gotson/komga.git
synced 2025-12-21 16:03:03 +01:00
feat(api): on-th-fly thumbnail generation for any page
This commit is contained in:
parent
ec06955e22
commit
7167f3ea24
3 changed files with 80 additions and 25 deletions
|
|
@ -62,33 +62,44 @@ class BookLifecycle(
|
|||
MediaNotReadyException::class,
|
||||
IndexOutOfBoundsException::class
|
||||
)
|
||||
fun getBookPage(book: Book, number: Int, convertTo: ImageType? = null): BookPageContent {
|
||||
fun getBookPage(book: Book, number: Int, convertTo: ImageType? = null, resizeTo: Int? = null): BookPageContent {
|
||||
val pageContent = bookAnalyzer.getPageContent(book, number)
|
||||
val pageMediaType = book.media.pages[number - 1].mediaType
|
||||
|
||||
convertTo?.let {
|
||||
val msg = "Convert page #$number of book $book from $pageMediaType to ${it.mediaType}"
|
||||
if (!imageConverter.supportedReadMediaTypes.contains(pageMediaType)) {
|
||||
throw ImageConversionException("$msg: unsupported read format $pageMediaType")
|
||||
}
|
||||
if (!imageConverter.supportedWriteMediaTypes.contains(it.mediaType)) {
|
||||
throw ImageConversionException("$msg: unsupported write format ${it.mediaType}")
|
||||
}
|
||||
if (pageMediaType == it.mediaType) {
|
||||
logger.warn { "$msg: same format, no need for conversion" }
|
||||
return@let
|
||||
}
|
||||
|
||||
logger.info { msg }
|
||||
if (resizeTo != null) {
|
||||
val targetFormat = ImageType.JPEG
|
||||
val convertedPage = try {
|
||||
imageConverter.convertImage(pageContent, it.imageIOFormat)
|
||||
imageConverter.resizeImage(pageContent, targetFormat.imageIOFormat, resizeTo)
|
||||
} catch (e: Exception) {
|
||||
logger.error(e) { "$msg: conversion failed" }
|
||||
logger.error(e) { "Resize page #$number of book $book to $resizeTo: failed" }
|
||||
throw e
|
||||
}
|
||||
return BookPageContent(number, convertedPage, it.mediaType)
|
||||
}
|
||||
return BookPageContent(number, convertedPage, targetFormat.mediaType)
|
||||
} else {
|
||||
convertTo?.let {
|
||||
val msg = "Convert page #$number of book $book from $pageMediaType to ${it.mediaType}"
|
||||
if (!imageConverter.supportedReadMediaTypes.contains(pageMediaType)) {
|
||||
throw ImageConversionException("$msg: unsupported read format $pageMediaType")
|
||||
}
|
||||
if (!imageConverter.supportedWriteMediaTypes.contains(it.mediaType)) {
|
||||
throw ImageConversionException("$msg: unsupported write format ${it.mediaType}")
|
||||
}
|
||||
if (pageMediaType == it.mediaType) {
|
||||
logger.warn { "$msg: same format, no need for conversion" }
|
||||
return@let
|
||||
}
|
||||
|
||||
return BookPageContent(number, pageContent, pageMediaType)
|
||||
logger.info { msg }
|
||||
val convertedPage = try {
|
||||
imageConverter.convertImage(pageContent, it.imageIOFormat)
|
||||
} catch (e: Exception) {
|
||||
logger.error(e) { "$msg: conversion failed" }
|
||||
throw e
|
||||
}
|
||||
return BookPageContent(number, convertedPage, it.mediaType)
|
||||
}
|
||||
|
||||
return BookPageContent(number, pageContent, pageMediaType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package org.gotson.komga.infrastructure.image
|
||||
|
||||
import mu.KotlinLogging
|
||||
import net.coobird.thumbnailator.Thumbnails
|
||||
import org.springframework.stereotype.Service
|
||||
import java.io.ByteArrayOutputStream
|
||||
import javax.imageio.ImageIO
|
||||
|
|
@ -23,9 +24,18 @@ class ImageConverter {
|
|||
}
|
||||
|
||||
fun convertImage(imageBytes: ByteArray, format: String): ByteArray =
|
||||
ByteArrayOutputStream().use {
|
||||
val image = ImageIO.read(imageBytes.inputStream())
|
||||
ImageIO.write(image, format, it)
|
||||
it.toByteArray()
|
||||
}
|
||||
ByteArrayOutputStream().use {
|
||||
val image = ImageIO.read(imageBytes.inputStream())
|
||||
ImageIO.write(image, format, it)
|
||||
it.toByteArray()
|
||||
}
|
||||
|
||||
fun resizeImage(imageBytes: ByteArray, format: String, size: Int): ByteArray =
|
||||
ByteArrayOutputStream().use {
|
||||
Thumbnails.of(imageBytes.inputStream())
|
||||
.size(size, size)
|
||||
.outputFormat(format)
|
||||
.toOutputStream(it)
|
||||
it.toByteArray()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -241,6 +241,40 @@ class BookController(
|
|||
}
|
||||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
|
||||
@GetMapping("api/v1/books/{bookId}/pages/{pageNumber}/thumbnail")
|
||||
fun getBookPageThumbnail(
|
||||
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||
request: WebRequest,
|
||||
@PathVariable bookId: Long,
|
||||
@PathVariable pageNumber: Int
|
||||
): ResponseEntity<ByteArray> =
|
||||
bookRepository.findByIdOrNull((bookId))?.let { book ->
|
||||
if (request.checkNotModified(getBookLastModified(book))) {
|
||||
return@let ResponseEntity
|
||||
.status(HttpStatus.NOT_MODIFIED)
|
||||
.setNotModified(book)
|
||||
.body(ByteArray(0))
|
||||
}
|
||||
if (!principal.user.canAccessBook(book)) throw ResponseStatusException(HttpStatus.UNAUTHORIZED)
|
||||
try {
|
||||
val pageContent = bookLifecycle.getBookPage(book, pageNumber, resizeTo = 300)
|
||||
|
||||
ResponseEntity.ok()
|
||||
.contentType(getMediaTypeOrDefault(pageContent.mediaType))
|
||||
.setNotModified(book)
|
||||
.body(pageContent.content)
|
||||
} catch (ex: IndexOutOfBoundsException) {
|
||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Page number does not exist")
|
||||
} catch (ex: ImageConversionException) {
|
||||
throw ResponseStatusException(HttpStatus.NOT_FOUND, ex.message)
|
||||
} catch (ex: MediaNotReadyException) {
|
||||
throw ResponseStatusException(HttpStatus.NOT_FOUND, "Book analysis failed")
|
||||
} catch (ex: NoSuchFileException) {
|
||||
logger.warn(ex) { "File not found: $book" }
|
||||
throw ResponseStatusException(HttpStatus.NOT_FOUND, "File not found, it may have moved")
|
||||
}
|
||||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
|
||||
@PostMapping("api/v1/books/{bookId}/analyze")
|
||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||
@ResponseStatus(HttpStatus.ACCEPTED)
|
||||
|
|
|
|||
Loading…
Reference in a new issue