From f9d55ecfd0743a3c0e07f5f63725d25e6183af0c Mon Sep 17 00:00:00 2001 From: Gauthier Roebroeck Date: Fri, 31 Jul 2020 09:15:48 +0800 Subject: [PATCH] feat: get images dimension during media analysis media analysis will get width and height for each image in a book this is required for #123 --- .../V20200730135746__image_dimension.sql | 4 +++ .../org/gotson/komga/domain/model/BookPage.kt | 3 +- .../gotson/komga/domain/model/Dimension.kt | 6 ++++ .../komga/domain/model/MediaContainerEntry.kt | 3 +- .../komga/domain/service/BookAnalyzer.kt | 2 +- .../infrastructure/image/ImageAnalyzer.kt | 28 +++++++++++++++++++ .../komga/infrastructure/jooq/MediaDao.kt | 14 +++++++--- .../mediacontainer/EpubExtractor.kt | 23 ++++++++++++--- .../mediacontainer/PdfExtractor.kt | 21 +++++++++++--- .../mediacontainer/RarExtractor.kt | 23 +++++++++++---- .../mediacontainer/ZipExtractor.kt | 13 +++++++-- .../komga/interfaces/rest/BookController.kt | 4 ++- .../komga/interfaces/rest/dto/PageDto.kt | 4 ++- 13 files changed, 123 insertions(+), 25 deletions(-) create mode 100644 komga/src/flyway/resources/db/migration/sqlite/V20200730135746__image_dimension.sql create mode 100644 komga/src/main/kotlin/org/gotson/komga/domain/model/Dimension.kt create mode 100644 komga/src/main/kotlin/org/gotson/komga/infrastructure/image/ImageAnalyzer.kt diff --git a/komga/src/flyway/resources/db/migration/sqlite/V20200730135746__image_dimension.sql b/komga/src/flyway/resources/db/migration/sqlite/V20200730135746__image_dimension.sql new file mode 100644 index 000000000..ac166e78a --- /dev/null +++ b/komga/src/flyway/resources/db/migration/sqlite/V20200730135746__image_dimension.sql @@ -0,0 +1,4 @@ +alter table media_page + add column width int NULL; +alter table media_page + add column height int NULL; diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/model/BookPage.kt b/komga/src/main/kotlin/org/gotson/komga/domain/model/BookPage.kt index 015ce450f..989884c65 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/model/BookPage.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/model/BookPage.kt @@ -2,5 +2,6 @@ package org.gotson.komga.domain.model data class BookPage( val fileName: String, - val mediaType: String + val mediaType: String, + val dimension: Dimension? = null ) diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/model/Dimension.kt b/komga/src/main/kotlin/org/gotson/komga/domain/model/Dimension.kt new file mode 100644 index 000000000..ff8f1f430 --- /dev/null +++ b/komga/src/main/kotlin/org/gotson/komga/domain/model/Dimension.kt @@ -0,0 +1,6 @@ +package org.gotson.komga.domain.model + +data class Dimension( + val width: Int, + val height: Int +) diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/model/MediaContainerEntry.kt b/komga/src/main/kotlin/org/gotson/komga/domain/model/MediaContainerEntry.kt index 496621632..9d47f0f15 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/model/MediaContainerEntry.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/model/MediaContainerEntry.kt @@ -3,5 +3,6 @@ package org.gotson.komga.domain.model data class MediaContainerEntry( val name: String, val mediaType: String? = null, - val comment: String? = null + val comment: String? = null, + val dimension: Dimension? = null ) diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/service/BookAnalyzer.kt b/komga/src/main/kotlin/org/gotson/komga/domain/service/BookAnalyzer.kt index 89c1d45e0..98636cf7b 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/service/BookAnalyzer.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/service/BookAnalyzer.kt @@ -51,7 +51,7 @@ class BookAnalyzer( entry.mediaType?.let { contentDetector.isImage(it) } ?: false }.let { (images, others) -> Pair( - images.map { BookPage(it.name, it.mediaType!!) }, + images.map { BookPage(it.name, it.mediaType!!, it.dimension) }, others ) } diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/image/ImageAnalyzer.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/image/ImageAnalyzer.kt new file mode 100644 index 000000000..57ec48cdb --- /dev/null +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/image/ImageAnalyzer.kt @@ -0,0 +1,28 @@ +package org.gotson.komga.infrastructure.image + +import mu.KotlinLogging +import org.gotson.komga.domain.model.Dimension +import org.springframework.stereotype.Service +import java.io.InputStream +import javax.imageio.ImageIO + +private val logger = KotlinLogging.logger {} + +@Service +class ImageAnalyzer { + + fun getDimension(stream: InputStream): Dimension? = + stream.use { + ImageIO.createImageInputStream(stream).use { fis -> + val readers = ImageIO.getImageReaders(fis) + if (readers.hasNext()) { + val reader = readers.next() + reader.input = fis + Dimension(reader.getWidth(0), reader.getHeight(0)) + } else { + logger.warn { "no reader found" } + null + } + } + } +} diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/MediaDao.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/MediaDao.kt index 31fa29bfd..f96e88979 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/MediaDao.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/MediaDao.kt @@ -1,6 +1,7 @@ package org.gotson.komga.infrastructure.jooq import org.gotson.komga.domain.model.BookPage +import org.gotson.komga.domain.model.Dimension import org.gotson.komga.domain.model.Media import org.gotson.komga.domain.persistence.MediaRepository import org.gotson.komga.jooq.Tables @@ -94,8 +95,10 @@ class MediaDao( p.BOOK_ID, p.FILE_NAME, p.MEDIA_TYPE, - p.NUMBER - ).values(null as String?, null, null, null) + p.NUMBER, + p.WIDTH, + p.HEIGHT + ).values(null as String?, null, null, null, null, null) ).also { medias.forEach { media -> media.pages.forEachIndexed { index, page -> @@ -103,7 +106,9 @@ class MediaDao( media.bookId, page.fileName, page.mediaType, - index + index, + page.dimension?.width, + page.dimension?.height ) } } @@ -198,6 +203,7 @@ class MediaDao( private fun MediaPageRecord.toDomain() = BookPage( fileName = fileName, - mediaType = mediaType + mediaType = mediaType, + dimension = if (width != null && height != null) Dimension(width, height) else null ) } diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/mediacontainer/EpubExtractor.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/mediacontainer/EpubExtractor.kt index 32c8b6852..b49888516 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/mediacontainer/EpubExtractor.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/mediacontainer/EpubExtractor.kt @@ -4,6 +4,7 @@ import mu.KotlinLogging import org.apache.commons.compress.archivers.zip.ZipFile import org.apache.commons.io.FilenameUtils import org.gotson.komga.domain.model.MediaContainerEntry +import org.gotson.komga.infrastructure.image.ImageAnalyzer import org.jsoup.Jsoup import org.springframework.stereotype.Service import java.nio.file.Path @@ -12,7 +13,11 @@ import java.nio.file.Paths private val logger = KotlinLogging.logger {} @Service -class EpubExtractor(contentDetector: ContentDetector) : ZipExtractor(contentDetector) { +class EpubExtractor( + private val zipExtractor: ZipExtractor, + private val contentDetector: ContentDetector, + private val imageAnalyzer: ImageAnalyzer +) : MediaContainerExtractor { override fun mediaTypes(): List = listOf("application/epub+zip") @@ -41,17 +46,27 @@ class EpubExtractor(contentDetector: ContentDetector) : ZipExtractor(contentDete } return images.map { image -> - MediaContainerEntry(image.separatorsToUnix(), manifest.values.first { + val name = image.separatorsToUnix() + val mediaType = manifest.values.first { it.href == (opfDir?.relativize(image) ?: image).separatorsToUnix() - }.mediaType) + }.mediaType + val dimension = if (contentDetector.isImage(mediaType)) + imageAnalyzer.getDimension(zip.getInputStream(zip.getEntry(name))) + else + null + MediaContainerEntry(name = name, mediaType = mediaType, dimension = dimension) } } catch (e: Exception) { logger.error(e) { "File is not a proper Epub, treating it as a zip file" } - return super.getEntries(path) + return zipExtractor.getEntries(path) } } } + override fun getEntryStream(path: Path, entryName: String): ByteArray { + return zipExtractor.getEntryStream(path, entryName) + } + private fun getPackagePath(zip: ZipFile): String = zip.getEntry("META-INF/container.xml").let { entry -> val container = zip.getInputStream(entry).use { Jsoup.parse(it, null, "") } diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/mediacontainer/PdfExtractor.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/mediacontainer/PdfExtractor.kt index f3d7600ba..70e4ffcab 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/mediacontainer/PdfExtractor.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/mediacontainer/PdfExtractor.kt @@ -1,16 +1,25 @@ package org.gotson.komga.infrastructure.mediacontainer +import mu.KotlinLogging import org.apache.pdfbox.pdmodel.PDDocument +import org.apache.pdfbox.pdmodel.PDPage import org.apache.pdfbox.rendering.ImageType import org.apache.pdfbox.rendering.PDFRenderer +import org.gotson.komga.domain.model.Dimension import org.gotson.komga.domain.model.MediaContainerEntry +import org.gotson.komga.infrastructure.image.ImageAnalyzer import org.springframework.stereotype.Service import java.io.ByteArrayOutputStream import java.nio.file.Path import javax.imageio.ImageIO +import kotlin.math.roundToInt + +private val logger = KotlinLogging.logger {} @Service -class PdfExtractor : MediaContainerExtractor { +class PdfExtractor( + private val imageAnalyzer: ImageAnalyzer +) : MediaContainerExtractor { private val mediaType = "image/jpeg" private val imageIOFormat = "jpeg" @@ -21,7 +30,10 @@ class PdfExtractor : MediaContainerExtractor { override fun getEntries(path: Path): List = PDDocument.load(path.toFile()).use { pdf -> (0 until pdf.numberOfPages).map { index -> - MediaContainerEntry(index.toString(), mediaType) + val page = pdf.getPage(index) + val scale = page.getScale() + val dimension = Dimension((page.cropBox.width * scale).roundToInt(), (page.cropBox.height * scale).roundToInt()) + MediaContainerEntry(name = index.toString(), mediaType = mediaType, dimension = dimension) } } @@ -29,11 +41,12 @@ class PdfExtractor : MediaContainerExtractor { PDDocument.load(path.toFile()).use { pdf -> val pageNumber = entryName.toInt() val page = pdf.getPage(pageNumber) - val scale = resolution / minOf(page.cropBox.width, page.cropBox.height) - val image = PDFRenderer(pdf).renderImage(pageNumber, scale, ImageType.RGB) + val image = PDFRenderer(pdf).renderImage(pageNumber, page.getScale(), ImageType.RGB) ByteArrayOutputStream().use { out -> ImageIO.write(image, imageIOFormat, out) out.toByteArray() } } + + private fun PDPage.getScale() = resolution / minOf(cropBox.width, cropBox.height) } diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/mediacontainer/RarExtractor.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/mediacontainer/RarExtractor.kt index 8681b922c..60721e41d 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/mediacontainer/RarExtractor.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/mediacontainer/RarExtractor.kt @@ -3,9 +3,13 @@ package org.gotson.komga.infrastructure.mediacontainer import com.github.junrar.Archive import mu.KotlinLogging import net.greypanther.natsort.CaseInsensitiveSimpleNaturalComparator +import org.apache.commons.io.input.TeeInputStream import org.gotson.komga.domain.model.MediaContainerEntry import org.gotson.komga.domain.model.MediaUnsupportedException +import org.gotson.komga.infrastructure.image.ImageAnalyzer import org.springframework.stereotype.Service +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream import java.nio.file.Path import java.util.Comparator @@ -13,7 +17,8 @@ private val logger = KotlinLogging.logger {} @Service class RarExtractor( - private val contentDetector: ContentDetector + private val contentDetector: ContentDetector, + private val imageAnalyzer: ImageAnalyzer ) : MediaContainerExtractor { private val natSortComparator: Comparator = CaseInsensitiveSimpleNaturalComparator.getInstance() @@ -27,12 +32,20 @@ class RarExtractor( if (rar.mainHeader.isMultiVolume) throw MediaUnsupportedException("Multi-Volume RAR archives are not supported") rar.fileHeaders .filter { !it.isDirectory } - .map { + .map { hd -> try { - MediaContainerEntry(name = it.fileName, mediaType = contentDetector.detectMediaType(rar.getInputStream(it))) + val buffer = ByteArrayOutputStream() + TeeInputStream(rar.getInputStream(hd), buffer).use { tee -> + val mediaType = contentDetector.detectMediaType(tee) + val dimension = if (contentDetector.isImage(mediaType)) + imageAnalyzer.getDimension(ByteArrayInputStream(buffer.toByteArray())) + else + null + MediaContainerEntry(name = hd.fileName, mediaType = mediaType, dimension = dimension) + } } catch (e: Exception) { - logger.warn(e) { "Could not analyze entry: ${it.fileName}" } - MediaContainerEntry(name = it.fileName, comment = e.message) + logger.warn(e) { "Could not analyze entry: ${hd.fileName}" } + MediaContainerEntry(name = hd.fileName, comment = e.message) } } .sortedWith(compareBy(natSortComparator) { it.name }) diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/mediacontainer/ZipExtractor.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/mediacontainer/ZipExtractor.kt index 274fc9481..6eea39595 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/mediacontainer/ZipExtractor.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/mediacontainer/ZipExtractor.kt @@ -4,15 +4,17 @@ import mu.KotlinLogging import net.greypanther.natsort.CaseInsensitiveSimpleNaturalComparator import org.apache.commons.compress.archivers.zip.ZipFile import org.gotson.komga.domain.model.MediaContainerEntry +import org.gotson.komga.infrastructure.image.ImageAnalyzer import org.springframework.stereotype.Service import java.nio.file.Path -import java.util.* +import java.util.Comparator private val logger = KotlinLogging.logger {} @Service class ZipExtractor( - private val contentDetector: ContentDetector + private val contentDetector: ContentDetector, + private val imageAnalyzer: ImageAnalyzer ) : MediaContainerExtractor { private val natSortComparator: Comparator = CaseInsensitiveSimpleNaturalComparator.getInstance() @@ -25,7 +27,12 @@ class ZipExtractor( .filter { !it.isDirectory } .map { try { - MediaContainerEntry(name = it.name, mediaType = contentDetector.detectMediaType(zip.getInputStream(it))) + val mediaType = contentDetector.detectMediaType(zip.getInputStream(it)) + val dimension = if (contentDetector.isImage(mediaType)) + imageAnalyzer.getDimension(zip.getInputStream(it)) + else + null + MediaContainerEntry(name = it.name, mediaType = mediaType, dimension = dimension) } catch (e: Exception) { logger.warn(e) { "Could not analyze entry: ${it.name}" } MediaContainerEntry(name = it.name, comment = e.message) 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 7208273fb..4cf3523c3 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 @@ -248,7 +248,9 @@ class BookController( Media.Status.OUTDATED -> throw ResponseStatusException(HttpStatus.NOT_FOUND, "Book is outdated and must be re-analyzed") Media.Status.ERROR -> throw ResponseStatusException(HttpStatus.NOT_FOUND, "Book analysis failed") Media.Status.UNSUPPORTED -> throw ResponseStatusException(HttpStatus.NOT_FOUND, "Book format is not supported") - Media.Status.READY -> media.pages.mapIndexed { index, s -> PageDto(index + 1, s.fileName, s.mediaType) } + Media.Status.READY -> media.pages.mapIndexed { index, s -> + PageDto(index + 1, s.fileName, s.mediaType, s.dimension?.width, s.dimension?.height) + } } } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/dto/PageDto.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/dto/PageDto.kt index 942d38ab6..1195ccc1c 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/dto/PageDto.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/dto/PageDto.kt @@ -3,5 +3,7 @@ package org.gotson.komga.interfaces.rest.dto data class PageDto( val number: Int, val fileName: String, - val mediaType: String + val mediaType: String, + val width: Int?, + val height: Int? )