Remote Image Processing (#1684)
* Update ServerConfig.kt * Update ConversionUtil.kt * Update Page.kt * Update ServerConfig.kt fixed deletions caused by ide * Update ServerConfig.kt * Update ServerConfig.kt * Cleanup * Post-processing terminology * More comments * Lint * Add known image mimes * Fix weird mime set/get * Implement different downloadConversions and serveConversions * Lint * Improve Post-Processing massivly * Fix thumbnail build * Use Array for headers * Actually fix headers * Actually fix headers 2 * Manually parse DownloadConversion * Cleanup parse * Fix write * Update TypeName * Optimize imports * Remove header type * Fix build --------- Co-authored-by: Syer10 <syer10@users.noreply.github.com>
This commit is contained in:
@@ -475,7 +475,7 @@ object MangaController {
|
||||
|
||||
ctx.future {
|
||||
future {
|
||||
Page.getPageImage(
|
||||
Page.getPageImageServe(
|
||||
mangaId = mangaId,
|
||||
chapterIndex = chapterIndex,
|
||||
index = index,
|
||||
|
||||
@@ -10,6 +10,7 @@ package suwayomi.tachidesk.manga.impl
|
||||
import eu.kanade.tachiyomi.source.local.LocalSource
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import libcore.net.MimeUtils
|
||||
import org.jetbrains.exposed.sql.SortOrder
|
||||
@@ -17,6 +18,7 @@ import org.jetbrains.exposed.sql.and
|
||||
import org.jetbrains.exposed.sql.selectAll
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import org.jetbrains.exposed.sql.update
|
||||
import suwayomi.tachidesk.graphql.types.DownloadConversion
|
||||
import suwayomi.tachidesk.manga.impl.util.getChapterCachePath
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
|
||||
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.getImageResponse
|
||||
@@ -24,14 +26,20 @@ import suwayomi.tachidesk.manga.impl.util.storage.ImageUtil
|
||||
import suwayomi.tachidesk.manga.model.table.ChapterTable
|
||||
import suwayomi.tachidesk.manga.model.table.MangaTable
|
||||
import suwayomi.tachidesk.manga.model.table.PageTable
|
||||
import suwayomi.tachidesk.server.serverConfig
|
||||
import suwayomi.tachidesk.util.ConversionUtil
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import javax.imageio.IIOImage
|
||||
import javax.imageio.ImageIO
|
||||
import javax.imageio.ImageWriteParam
|
||||
import javax.imageio.ImageWriter
|
||||
|
||||
object Page {
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
/**
|
||||
* A page might have a imageUrl ready from the get go, or we might need to
|
||||
* go an extra step and call fetchImageUrl to get it.
|
||||
@@ -51,7 +59,6 @@ object Page {
|
||||
chapterId: Int? = null,
|
||||
chapterIndex: Int? = null,
|
||||
index: Int,
|
||||
format: String? = null,
|
||||
progressFlow: ((StateFlow<Int>) -> Unit)? = null,
|
||||
): Pair<InputStream, String> {
|
||||
val mangaEntry = transaction { MangaTable.selectAll().where { MangaTable.id eq mangaId }.first() }
|
||||
@@ -73,7 +80,7 @@ object Page {
|
||||
|
||||
try {
|
||||
if (chapterEntry[ChapterTable.isDownloaded]) {
|
||||
return convertImageResponse(ChapterDownloadHelper.getImage(mangaId, chapterId, index), format)
|
||||
return ChapterDownloadHelper.getImage(mangaId, chapterId, index)
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
// ignore and fetch again
|
||||
@@ -102,15 +109,12 @@ object Page {
|
||||
// is of archive format
|
||||
if (LocalSource.pageCache.containsKey(chapterEntry[ChapterTable.url])) {
|
||||
val pageStream = LocalSource.pageCache[chapterEntry[ChapterTable.url]]!![index]
|
||||
return convertImageResponse(pageStream() to (ImageUtil.findImageType { pageStream() }?.mime ?: "image/jpeg"), format)
|
||||
return pageStream() to (ImageUtil.findImageType { pageStream() }?.mime ?: "image/jpeg")
|
||||
}
|
||||
|
||||
// is of directory format
|
||||
val imageFile = File(tachiyomiPage.imageUrl!!)
|
||||
return convertImageResponse(
|
||||
imageFile.inputStream() to (ImageUtil.findImageType { imageFile.inputStream() }?.mime ?: "image/jpeg"),
|
||||
format,
|
||||
)
|
||||
return imageFile.inputStream() to (ImageUtil.findImageType { imageFile.inputStream() }?.mime ?: "image/jpeg")
|
||||
}
|
||||
|
||||
val source = getCatalogueSourceOrStub(mangaEntry[MangaTable.sourceReference])
|
||||
@@ -130,38 +134,210 @@ object Page {
|
||||
val cacheSaveDir = getChapterCachePath(mangaId, chapterId)
|
||||
|
||||
// Note: don't care about invalidating cache because OS cache is not permanent
|
||||
return convertImageResponse(
|
||||
getImageResponse(cacheSaveDir, fileName) {
|
||||
source.getImage(tachiyomiPage)
|
||||
},
|
||||
format,
|
||||
)
|
||||
return getImageResponse(cacheSaveDir, fileName) {
|
||||
source.getImage(tachiyomiPage)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getPageImageServe(
|
||||
mangaId: Int,
|
||||
chapterIndex: Int,
|
||||
index: Int,
|
||||
format: String? = null,
|
||||
): Pair<InputStream, String> {
|
||||
val (inputStream, mime) =
|
||||
getPageImage(
|
||||
mangaId = mangaId,
|
||||
chapterIndex = chapterIndex,
|
||||
index = index,
|
||||
)
|
||||
val conversions = serverConfig.serveConversions.value
|
||||
val defaultConversion = conversions["default"]
|
||||
val formatConversion = format?.let { DownloadConversion(target = it) }
|
||||
val conversion =
|
||||
formatConversion
|
||||
?: conversions[mime]
|
||||
?: defaultConversion
|
||||
?: return inputStream to mime
|
||||
|
||||
val converted =
|
||||
try {
|
||||
convertImageResponse(
|
||||
image = inputStream,
|
||||
mime = mime,
|
||||
conversion = conversion,
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logger.error(e) { "Error while post-processing image" }
|
||||
null
|
||||
}
|
||||
return converted?.also { inputStream.close() } ?: (inputStream to mime)
|
||||
}
|
||||
|
||||
suspend fun getPageImageDownload(
|
||||
mangaId: Int,
|
||||
chapterId: Int,
|
||||
index: Int,
|
||||
downloadCacheFolder: File,
|
||||
fileName: String,
|
||||
progressFlow: (StateFlow<Int>) -> Unit,
|
||||
) {
|
||||
val (inputStream, mime) =
|
||||
getPageImage(
|
||||
mangaId = mangaId,
|
||||
chapterId = chapterId,
|
||||
index = index,
|
||||
progressFlow = progressFlow,
|
||||
)
|
||||
val conversions = serverConfig.downloadConversions.value
|
||||
if (conversions.isEmpty() || !downloadCacheFolder.exists()) {
|
||||
inputStream.close()
|
||||
return
|
||||
}
|
||||
val defaultConversion = conversions["default"]
|
||||
val conversion =
|
||||
conversions[mime]
|
||||
?: defaultConversion
|
||||
if (conversion == null) {
|
||||
inputStream.close()
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
val converted =
|
||||
try {
|
||||
convertImageResponse(
|
||||
image = inputStream,
|
||||
mime = mime,
|
||||
conversion = conversion,
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
throw e
|
||||
} finally {
|
||||
inputStream.close()
|
||||
}
|
||||
|
||||
if (converted != null) {
|
||||
val (convertedStream, convertedMime) = converted
|
||||
val convertedExtension =
|
||||
MimeUtils.guessExtensionFromMimeType(convertedMime)
|
||||
?: convertedMime.substringAfter('/')
|
||||
val convertedPage =
|
||||
File(
|
||||
downloadCacheFolder,
|
||||
"$fileName.$convertedExtension",
|
||||
)
|
||||
|
||||
convertedPage.outputStream().use { outputStream ->
|
||||
convertedStream.use { it.copyTo(outputStream) }
|
||||
}
|
||||
|
||||
val extension =
|
||||
MimeUtils.guessExtensionFromMimeType(mime)
|
||||
?: mime.substringAfter('/')
|
||||
if (extension != convertedExtension) {
|
||||
File(
|
||||
downloadCacheFolder,
|
||||
"$fileName.$extension",
|
||||
).delete()
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.warn(e) { "Error while post-processing image" }
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun convertImageResponse(
|
||||
image: Pair<InputStream, String>,
|
||||
format: String? = null,
|
||||
): Pair<InputStream, String> {
|
||||
val imageExtension = MimeUtils.guessExtensionFromMimeType(image.second) ?: image.second.removePrefix("image/")
|
||||
image: InputStream,
|
||||
mime: String,
|
||||
conversion: DownloadConversion,
|
||||
): Pair<InputStream, String>? {
|
||||
// Apply HTTP post-process if configured (complementary with format conversion)
|
||||
if (ConversionUtil.isHttpPostProcess(conversion)) {
|
||||
try {
|
||||
val processedStream =
|
||||
ConversionUtil
|
||||
.imageHttpPostProcess(
|
||||
inputStream = image,
|
||||
mimeType = mime,
|
||||
conversion = conversion,
|
||||
)?.buffered()
|
||||
if (processedStream != null) {
|
||||
val mime =
|
||||
ImageUtil.findImageType(processedStream)?.mime
|
||||
?: "image/jpeg"
|
||||
|
||||
val targetExtension =
|
||||
(if (format != imageExtension) format else null)
|
||||
?: return image
|
||||
return processedStream to mime
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// HTTP post-processing failed, continue with original image
|
||||
logger.warn(e) { "Error while post-processing image" }
|
||||
}
|
||||
return null
|
||||
} else {
|
||||
if (mime == conversion.target) {
|
||||
return null
|
||||
}
|
||||
|
||||
val outStream = ByteArrayOutputStream()
|
||||
val writers = ImageIO.getImageWritersBySuffix(targetExtension)
|
||||
val writer = writers.next()
|
||||
ImageIO.createImageOutputStream(outStream).use { o ->
|
||||
writer.setOutput(o)
|
||||
|
||||
val inImage =
|
||||
ConversionUtil.readImage(image.first, image.second)
|
||||
?: throw NoSuchElementException("No conversion to $targetExtension possible")
|
||||
writer.write(inImage)
|
||||
return convertToFormat(image, mime, conversion)
|
||||
}
|
||||
}
|
||||
|
||||
private fun convertToFormat(
|
||||
inputStream: InputStream,
|
||||
sourceMimeType: String,
|
||||
target: DownloadConversion,
|
||||
): Pair<InputStream, String>? {
|
||||
val outStream = ByteArrayOutputStream()
|
||||
val conversionWriter =
|
||||
getConversionWriter(
|
||||
target.target,
|
||||
target.compressionLevel,
|
||||
)
|
||||
if (conversionWriter == null) {
|
||||
logger.warn { "Conversion aborted: No reader for target format ${target.target}" }
|
||||
return inputStream to sourceMimeType
|
||||
}
|
||||
|
||||
val (writer, writerParams) = conversionWriter
|
||||
try {
|
||||
ImageIO.createImageOutputStream(outStream).use { o ->
|
||||
writer.setOutput(o)
|
||||
|
||||
val inImage =
|
||||
ConversionUtil.readImage(inputStream, sourceMimeType)
|
||||
?: throw NoSuchElementException("No conversion to ${target.target} possible")
|
||||
writer.write(null, IIOImage(inImage, null, null), writerParams)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.warn(e) { "Conversion aborted" }
|
||||
return null
|
||||
} finally {
|
||||
writer.dispose()
|
||||
}
|
||||
writer.dispose()
|
||||
val inStream = ByteArrayInputStream(outStream.toByteArray())
|
||||
return Pair(inStream.buffered(), MimeUtils.guessMimeTypeFromExtension(targetExtension) ?: "image/$targetExtension")
|
||||
return Pair(inStream.buffered(), target.target)
|
||||
}
|
||||
|
||||
private fun getConversionWriter(
|
||||
targetMime: String,
|
||||
compressionLevel: Double?,
|
||||
): Pair<ImageWriter, ImageWriteParam>? {
|
||||
val writers = ImageIO.getImageWritersByMIMEType(targetMime)
|
||||
val writer =
|
||||
try {
|
||||
writers.next()
|
||||
} catch (_: NoSuchElementException) {
|
||||
return null
|
||||
}
|
||||
|
||||
val writerParams = writer.defaultWriteParam
|
||||
compressionLevel?.let {
|
||||
writerParams.compressionMode = ImageWriteParam.MODE_EXPLICIT
|
||||
writerParams.compressionQuality = it.toFloat()
|
||||
}
|
||||
|
||||
return writer to writerParams
|
||||
}
|
||||
|
||||
/** converts 0 to "001" */
|
||||
|
||||
+4
-105
@@ -151,10 +151,12 @@ abstract class ChaptersFilesProvider<Type : FileType>(
|
||||
|
||||
try {
|
||||
Page
|
||||
.getPageImage(
|
||||
.getPageImageDownload(
|
||||
mangaId = download.mangaId,
|
||||
chapterId = download.chapterId,
|
||||
index = pageNum,
|
||||
downloadCacheFolder,
|
||||
fileName,
|
||||
) { flow ->
|
||||
pageProgressJob =
|
||||
flow
|
||||
@@ -167,8 +169,7 @@ abstract class ChaptersFilesProvider<Type : FileType>(
|
||||
false,
|
||||
) // don't throw on canceled download here since we can't do anything
|
||||
}.launchIn(scope)
|
||||
}.first
|
||||
.close()
|
||||
}
|
||||
} finally {
|
||||
// always cancel the page progress job even if it throws an exception to avoid memory leaks
|
||||
pageProgressJob?.cancel()
|
||||
@@ -188,8 +189,6 @@ abstract class ChaptersFilesProvider<Type : FileType>(
|
||||
},
|
||||
)
|
||||
|
||||
maybeConvertPages(downloadCacheFolder)
|
||||
|
||||
handleSuccessfulDownload()
|
||||
|
||||
// Calculate and save Koreader hash for CBZ files
|
||||
@@ -221,104 +220,4 @@ abstract class ChaptersFilesProvider<Type : FileType>(
|
||||
abstract fun getAsArchiveStream(): Pair<InputStream, Long>
|
||||
|
||||
abstract fun getArchiveSize(): Long
|
||||
|
||||
private fun maybeConvertPages(chapterCacheFolder: File) {
|
||||
val conversions = serverConfig.downloadConversions.value
|
||||
|
||||
if (!chapterCacheFolder.isDirectory || conversions.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
val pages =
|
||||
chapterCacheFolder
|
||||
.listFiles()
|
||||
.orEmpty()
|
||||
.filter { it.name != COMIC_INFO_FILE }
|
||||
|
||||
val pagesByMimeType =
|
||||
pages
|
||||
.groupBy { MimeUtils.guessMimeTypeFromExtension(it.extension) }
|
||||
.mapValues { it.value.map { it.nameWithoutExtension } }
|
||||
|
||||
logger.debug { "maybeConvertPages: pagesByMimeType= $pagesByMimeType; conversions= $conversions" }
|
||||
|
||||
pages.forEach { page ->
|
||||
val imageType = MimeUtils.guessMimeTypeFromExtension(page.extension) ?: return@forEach
|
||||
|
||||
val defaultConversion = conversions["default"]
|
||||
val conversion = conversions[imageType]
|
||||
val targetConversion = conversion ?: defaultConversion ?: return@forEach
|
||||
|
||||
val (targetMime) = targetConversion
|
||||
val requiresConversion = imageType != targetMime && targetMime != "none"
|
||||
if (!requiresConversion) {
|
||||
return@forEach
|
||||
}
|
||||
|
||||
convertPage(page, targetConversion)
|
||||
}
|
||||
}
|
||||
|
||||
private fun convertPage(
|
||||
page: File,
|
||||
conversion: DownloadConversion,
|
||||
) {
|
||||
val (targetMime, compressionLevel) = conversion
|
||||
|
||||
val targetExtension =
|
||||
MimeUtils.guessExtensionFromMimeType(targetMime) ?: targetMime.removePrefix("image/")
|
||||
|
||||
val convertedPage = File(page.parentFile, page.nameWithoutExtension + "." + targetExtension)
|
||||
|
||||
val conversionWriter = getConversionWriter(targetMime, compressionLevel)
|
||||
if (conversionWriter == null) {
|
||||
logger.warn { "Conversion aborted: No reader for target format $targetMime" }
|
||||
return
|
||||
}
|
||||
|
||||
val (writer, writerParams) = conversionWriter
|
||||
|
||||
val success =
|
||||
try {
|
||||
ImageIO.createImageOutputStream(convertedPage).use { outStream ->
|
||||
writer.setOutput(outStream)
|
||||
|
||||
val inImage = ConversionUtil.readImage(page) ?: return@use false
|
||||
writer.write(null, IIOImage(inImage, null, null), writerParams)
|
||||
|
||||
true
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.warn(e) { "Conversion aborted: for image $page" }
|
||||
false
|
||||
}
|
||||
writer.dispose()
|
||||
|
||||
if (success) {
|
||||
page.delete()
|
||||
} else {
|
||||
convertedPage.delete()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getConversionWriter(
|
||||
targetMime: String,
|
||||
compressionLevel: Double?,
|
||||
): Pair<ImageWriter, ImageWriteParam>? {
|
||||
val writers = ImageIO.getImageWritersByMIMEType(targetMime)
|
||||
val writer =
|
||||
try {
|
||||
writers.next()
|
||||
} catch (_: NoSuchElementException) {
|
||||
return null
|
||||
}
|
||||
|
||||
val writerParams = writer.defaultWriteParam
|
||||
compressionLevel?.let {
|
||||
writerParams.compressionMode = ImageWriteParam.MODE_EXPLICIT
|
||||
writerParams.compressionQuality = it.toFloat()
|
||||
}
|
||||
|
||||
return writer to writerParams
|
||||
}
|
||||
}
|
||||
|
||||
+3
-2
@@ -44,10 +44,11 @@ class ThumbnailFileProvider(
|
||||
return true
|
||||
}
|
||||
|
||||
Manga.fetchMangaThumbnail(mangaId).first.use { image ->
|
||||
val (inputStream, mime) = Manga.fetchMangaThumbnail(mangaId)
|
||||
inputStream.use { image ->
|
||||
makeSureDownloadDirExists()
|
||||
val filePath = getThumbnailDownloadPath(mangaId)
|
||||
ImageResponse.saveImage(filePath, image)
|
||||
ImageResponse.saveImage(filePath, image, mime)
|
||||
}
|
||||
|
||||
return true
|
||||
|
||||
@@ -7,6 +7,7 @@ package suwayomi.tachidesk.manga.impl.util.storage
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import libcore.net.MimeUtils
|
||||
import okhttp3.Response
|
||||
import okhttp3.internal.closeQuietly
|
||||
import java.io.File
|
||||
@@ -69,7 +70,12 @@ object ImageResponse {
|
||||
|
||||
try {
|
||||
if (response.code == 200) {
|
||||
val (actualSavePath, imageType) = saveImage(filePath, response.body.byteStream())
|
||||
val (actualSavePath, imageType) =
|
||||
saveImage(
|
||||
filePath,
|
||||
response.body.byteStream(),
|
||||
response.header("Content-Type"),
|
||||
)
|
||||
return pathToInputStream(actualSavePath) to imageType
|
||||
} else {
|
||||
throw Exception("request error! ${response.code}")
|
||||
@@ -87,20 +93,28 @@ object ImageResponse {
|
||||
fun saveImage(
|
||||
filePath: String,
|
||||
image: InputStream,
|
||||
mimeType: String?,
|
||||
): Pair<String, String> {
|
||||
val mimeType = mimeType?.takeIf { it.startsWith("image/") }?.lowercase()
|
||||
val tmpSavePath = "$filePath.tmp"
|
||||
val tmpSaveFile = File(tmpSavePath)
|
||||
image.use { input -> tmpSaveFile.outputStream().use { output -> input.copyTo(output) } }
|
||||
|
||||
// find image type
|
||||
val imageType =
|
||||
ImageUtil.findImageType { tmpSaveFile.inputStream() }?.mime
|
||||
?: "image/jpeg"
|
||||
ImageUtil.findImageType { tmpSaveFile.inputStream() }
|
||||
?: ImageUtil.ImageType.entries.find {
|
||||
it.mime == mimeType
|
||||
}
|
||||
val extension =
|
||||
imageType?.extension ?: mimeType?.let {
|
||||
MimeUtils.guessExtensionFromMimeType(it)
|
||||
} ?: "jpg"
|
||||
|
||||
val actualSavePath = "$filePath.${imageType.substringAfter("/")}"
|
||||
val actualSavePath = "$filePath.$extension"
|
||||
|
||||
tmpSaveFile.renameTo(File(actualSavePath))
|
||||
return Pair(actualSavePath, imageType)
|
||||
return Pair(actualSavePath, imageType?.mime ?: mimeType ?: "image/jpeg")
|
||||
}
|
||||
|
||||
fun clearCachedImage(
|
||||
|
||||
@@ -1,35 +1,28 @@
|
||||
package suwayomi.tachidesk.util
|
||||
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.network.await
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import libcore.net.MimeUtils
|
||||
import okhttp3.Headers
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.MultipartBody
|
||||
import okhttp3.RequestBody.Companion.asRequestBody
|
||||
import suwayomi.tachidesk.graphql.types.DownloadConversion
|
||||
import suwayomi.tachidesk.manga.impl.util.storage.ImageUtil
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.awt.image.BufferedImage
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.nio.file.Files
|
||||
import javax.imageio.ImageIO
|
||||
import kotlin.getValue
|
||||
|
||||
object ConversionUtil {
|
||||
val logger = KotlinLogging.logger {}
|
||||
|
||||
public fun readImage(image: File): BufferedImage? {
|
||||
val readers = ImageIO.getImageReadersBySuffix(image.extension)
|
||||
image.inputStream().use {
|
||||
ImageIO.createImageInputStream(it).use { inputStream ->
|
||||
for (reader in readers) {
|
||||
try {
|
||||
reader.setInput(inputStream)
|
||||
return reader.read(0)
|
||||
} catch (e: Throwable) {
|
||||
logger.debug(e) { "Reader ${reader.javaClass.name} not suitable" }
|
||||
} finally {
|
||||
reader.dispose()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
logger.info { "No suitable image converter found for ${image.name}" }
|
||||
return null
|
||||
}
|
||||
|
||||
public fun readImage(
|
||||
fun readImage(
|
||||
image: InputStream,
|
||||
mimeType: String,
|
||||
): BufferedImage? {
|
||||
@@ -49,4 +42,100 @@ object ConversionUtil {
|
||||
logger.info { "No suitable image converter found for $mimeType" }
|
||||
return null
|
||||
}
|
||||
|
||||
private val networkService: NetworkHelper by injectLazy()
|
||||
|
||||
/**
|
||||
* Send image to external HTTP service for post-processing
|
||||
* Returns the processed image stream or null if failed
|
||||
*/
|
||||
suspend fun imageHttpPostProcess(
|
||||
imageFile: File,
|
||||
conversion: DownloadConversion,
|
||||
mimeType: String,
|
||||
): InputStream? =
|
||||
try {
|
||||
logger.debug { "Sending ${imageFile.name} to HTTP converter: ${conversion.target}" }
|
||||
|
||||
val requestBody =
|
||||
MultipartBody
|
||||
.Builder()
|
||||
.setType(MultipartBody.FORM)
|
||||
.addFormDataPart(
|
||||
"image",
|
||||
imageFile.name,
|
||||
imageFile.asRequestBody(mimeType.toMediaType()),
|
||||
).build()
|
||||
|
||||
val client =
|
||||
networkService.client
|
||||
.newBuilder()
|
||||
.apply {
|
||||
if (conversion.callTimeout != null) {
|
||||
callTimeout(conversion.callTimeout!!)
|
||||
}
|
||||
if (conversion.connectTimeout != null) {
|
||||
connectTimeout(conversion.connectTimeout!!)
|
||||
}
|
||||
}.build()
|
||||
|
||||
val response =
|
||||
client
|
||||
.newCall(
|
||||
POST(
|
||||
conversion.target,
|
||||
body = requestBody,
|
||||
headers =
|
||||
Headers
|
||||
.Builder()
|
||||
.apply {
|
||||
conversion.headers?.forEach {
|
||||
set(it.key, it.value)
|
||||
}
|
||||
}.build(),
|
||||
),
|
||||
).await()
|
||||
logger.debug { "HTTP conversion successful for ${imageFile.name}" }
|
||||
response.body.byteStream()
|
||||
} catch (e: Exception) {
|
||||
logger.warn(e) { "HTTP conversion failed for ${imageFile.name}" }
|
||||
null
|
||||
}
|
||||
|
||||
/**
|
||||
* Overload that takes InputStream and mimeType, creates temp file for HTTP upload
|
||||
*/
|
||||
suspend fun imageHttpPostProcess(
|
||||
inputStream: InputStream,
|
||||
mimeType: String,
|
||||
conversion: DownloadConversion,
|
||||
): InputStream? =
|
||||
try {
|
||||
// Create temporary file from input stream
|
||||
val extension =
|
||||
MimeUtils.guessExtensionFromMimeType(mimeType)
|
||||
?: mimeType.substringAfter('/')
|
||||
|
||||
val tempFile = Files.createTempFile("conversion", ".$extension").toFile()
|
||||
tempFile.outputStream().use { output ->
|
||||
inputStream.copyTo(output)
|
||||
}
|
||||
|
||||
// Convert using file method
|
||||
val result = imageHttpPostProcess(tempFile, conversion, mimeType)
|
||||
|
||||
// Clean up temp file
|
||||
tempFile.delete()
|
||||
|
||||
result
|
||||
} catch (e: Exception) {
|
||||
logger.warn(e) { "Failed to create temp file for HTTP converter" }
|
||||
null
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a DownloadConversion target is an HTTP URL
|
||||
*/
|
||||
fun isHttpPostProcess(conversion: DownloadConversion): Boolean =
|
||||
conversion.target.startsWith("http://") || conversion.target.startsWith("https://")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user