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:
FadedSociety
2025-11-12 14:23:34 -07:00
committed by GitHub
parent 3e47859d88
commit 0a7e6cce87
16 changed files with 527 additions and 188 deletions

View File

@@ -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://")
}