diff --git a/komga/src/main/kotlin/org/gotson/komga/application/service/BookLifecycle.kt b/komga/src/main/kotlin/org/gotson/komga/application/service/BookLifecycle.kt index e98e2b550..ed1f4468c 100644 --- a/komga/src/main/kotlin/org/gotson/komga/application/service/BookLifecycle.kt +++ b/komga/src/main/kotlin/org/gotson/komga/application/service/BookLifecycle.kt @@ -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) + } } } diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/image/ImageConverter.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/image/ImageConverter.kt index 738e1c8c4..7ad987d33 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/image/ImageConverter.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/image/ImageConverter.kt @@ -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() + } } diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/BookController.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/BookController.kt index 5b9102898..16805a118 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/BookController.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/BookController.kt @@ -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 = + 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)