Skip to content

Commit

Permalink
feat(api): on-th-fly thumbnail generation for any page
Browse files Browse the repository at this point in the history
  • Loading branch information
gotson committed Jan 10, 2020
1 parent ec06955 commit 7167f3e
Show file tree
Hide file tree
Showing 3 changed files with 80 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

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)
return BookPageContent(number, pageContent, pageMediaType)
}
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit 7167f3e

Please sign in to comment.