feat(api): on-th-fly thumbnail generation for any page

This commit is contained in:
Gauthier Roebroeck 2020-01-10 14:28:13 +08:00
parent ec06955e22
commit 7167f3ea24
3 changed files with 80 additions and 25 deletions

View file

@ -62,10 +62,20 @@ class BookLifecycle(
MediaNotReadyException::class, MediaNotReadyException::class,
IndexOutOfBoundsException::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 pageContent = bookAnalyzer.getPageContent(book, number)
val pageMediaType = book.media.pages[number - 1].mediaType val pageMediaType = book.media.pages[number - 1].mediaType
if (resizeTo != null) {
val targetFormat = ImageType.JPEG
val convertedPage = try {
imageConverter.resizeImage(pageContent, targetFormat.imageIOFormat, resizeTo)
} catch (e: Exception) {
logger.error(e) { "Resize page #$number of book $book to $resizeTo: failed" }
throw e
}
return BookPageContent(number, convertedPage, targetFormat.mediaType)
} else {
convertTo?.let { convertTo?.let {
val msg = "Convert page #$number of book $book from $pageMediaType to ${it.mediaType}" val msg = "Convert page #$number of book $book from $pageMediaType to ${it.mediaType}"
if (!imageConverter.supportedReadMediaTypes.contains(pageMediaType)) { if (!imageConverter.supportedReadMediaTypes.contains(pageMediaType)) {
@ -91,4 +101,5 @@ class BookLifecycle(
return BookPageContent(number, pageContent, pageMediaType) return BookPageContent(number, pageContent, pageMediaType)
} }
}
} }

View file

@ -1,6 +1,7 @@
package org.gotson.komga.infrastructure.image package org.gotson.komga.infrastructure.image
import mu.KotlinLogging import mu.KotlinLogging
import net.coobird.thumbnailator.Thumbnails
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import javax.imageio.ImageIO import javax.imageio.ImageIO
@ -28,4 +29,13 @@ class ImageConverter {
ImageIO.write(image, format, it) ImageIO.write(image, format, it)
it.toByteArray() 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()
}
} }

View file

@ -241,6 +241,40 @@ class BookController(
} }
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) } ?: 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") @PostMapping("api/v1/books/{bookId}/analyze")
@PreAuthorize("hasRole('ROLE_ADMIN')") @PreAuthorize("hasRole('ROLE_ADMIN')")
@ResponseStatus(HttpStatus.ACCEPTED) @ResponseStatus(HttpStatus.ACCEPTED)