support for image conversion (fixes #6)

support for TIFF format
This commit is contained in:
Gauthier Roebroeck 2019-09-02 17:27:06 +08:00
parent 68467de034
commit 9b11e47ed2
6 changed files with 121 additions and 20 deletions

View file

@ -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")

View file

@ -0,0 +1,7 @@
package org.gotson.komga.domain.model
class BookPageContent(
val number: Int,
val content: ByteArray,
val mediaType: String
)

View file

@ -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)
}
}

View file

@ -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

View file

@ -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"
}

View file

@ -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<ByteArray> {
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<ByteArray> {
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(