From 9b11e47ed2846dc2624eda9fce0ca5fed71b030f Mon Sep 17 00:00:00 2001 From: Gauthier Roebroeck Date: Mon, 2 Sep 2019 17:27:06 +0800 Subject: [PATCH] support for image conversion (fixes #6) support for TIFF format --- komga/build.gradle.kts | 1 + .../komga/domain/model/BookPageContent.kt | 7 ++++ .../komga/domain/service/BookManager.kt | 36 +++++++++++++++- .../infrastructure/image/ImageConverter.kt | 36 ++++++++++++++++ .../komga/infrastructure/image/ImageType.kt | 19 +++++++++ .../komga/interfaces/web/SerieController.kt | 42 ++++++++++--------- 6 files changed, 121 insertions(+), 20 deletions(-) create mode 100644 komga/src/main/kotlin/org/gotson/komga/domain/model/BookPageContent.kt create mode 100644 komga/src/main/kotlin/org/gotson/komga/infrastructure/image/ImageConverter.kt create mode 100644 komga/src/main/kotlin/org/gotson/komga/infrastructure/image/ImageType.kt diff --git a/komga/build.gradle.kts b/komga/build.gradle.kts index dae973986..8be3cf5dc 100644 --- a/komga/build.gradle.kts +++ b/komga/build.gradle.kts @@ -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") diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/model/BookPageContent.kt b/komga/src/main/kotlin/org/gotson/komga/domain/model/BookPageContent.kt new file mode 100644 index 000000000..fc8ffb077 --- /dev/null +++ b/komga/src/main/kotlin/org/gotson/komga/domain/model/BookPageContent.kt @@ -0,0 +1,7 @@ +package org.gotson.komga.domain.model + +class BookPageContent( + val number: Int, + val content: ByteArray, + val mediaType: String +) \ No newline at end of file diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/service/BookManager.kt b/komga/src/main/kotlin/org/gotson/komga/domain/service/BookManager.kt index 2cb6a64d1..0bfa3171a 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/service/BookManager.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/service/BookManager.kt @@ -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) + } } \ No newline at end of file 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 new file mode 100644 index 000000000..60ae3253d --- /dev/null +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/image/ImageConverter.kt @@ -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 \ No newline at end of file diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/image/ImageType.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/image/ImageType.kt new file mode 100644 index 000000000..1b1050c9b --- /dev/null +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/image/ImageType.kt @@ -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" + } \ No newline at end of file diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/web/SerieController.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/web/SerieController.kt index ce7e8cf6f..fca4be4a8 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/web/SerieController.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/web/SerieController.kt @@ -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 { 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 { 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(