From 333cf86243ebd5e3693cbfe6a79fb2e2226cae42 Mon Sep 17 00:00:00 2001 From: MrPowerGamerBR Date: Mon, 14 Oct 2024 14:50:59 -0300 Subject: [PATCH] Allow using "original" to get the original image file, add support for jpeg files (downscaling not supported yet) --- .../etherealgambi/backend/EtherealGambi.kt | 16 +++- .../backend/routes/GetFileRoute.kt | 79 ++++++++++++------- .../backend/utils/JPEGImageInfo.kt | 61 ++++++++++++++ 3 files changed, 123 insertions(+), 33 deletions(-) create mode 100644 backend/src/main/kotlin/net/perfectdreams/etherealgambi/backend/utils/JPEGImageInfo.kt diff --git a/backend/src/main/kotlin/net/perfectdreams/etherealgambi/backend/EtherealGambi.kt b/backend/src/main/kotlin/net/perfectdreams/etherealgambi/backend/EtherealGambi.kt index cd0eaf1..3ea1835 100644 --- a/backend/src/main/kotlin/net/perfectdreams/etherealgambi/backend/EtherealGambi.kt +++ b/backend/src/main/kotlin/net/perfectdreams/etherealgambi/backend/EtherealGambi.kt @@ -149,11 +149,12 @@ class EtherealGambi(val config: EtherealGambiConfig) { when (imageType) { ImageType.PNG -> PNGImageInfo(this, data) ImageType.GIF -> GIFImageInfo(this, data) + ImageType.JPEG -> JPEGImageInfo(this, data) } } } - fun getVariantFromFileName(fileNameWithUnknownVariant: String): VariantResult { + fun getVariantFromFileName(pathWithoutFile: String, fileNameWithUnknownVariant: String): VariantResult { val nameWithoutExtension = fileNameWithUnknownVariant.substringBeforeLast(".") val extension = fileNameWithUnknownVariant.substringAfterLast(".") .lowercase() @@ -162,8 +163,13 @@ class EtherealGambi(val config: EtherealGambiConfig) { val variantResult = GetFileRoute.variantRegex.findAll(nameWithoutExtension) .lastOrNull() + if (variantResult != null && variantResult.groupValues[1] == "original") { + val fileNameWithoutTheVariant = nameWithoutExtension.replace("@original", "") + return OriginalFile("$pathWithoutFile/$fileNameWithoutTheVariant.$extension") + } + // GIF doesn't support scaling down... yet - if (variantResult != null && imageType != ImageType.GIF) { + if (variantResult != null && imageType != ImageType.GIF && imageType != ImageType.JPEG) { val variantPreset = scaleDownToWidthVariantsPresets.firstOrNull { it.name == variantResult.groupValues[1] } ?: return VariantNotFound val fileNameWithoutTheVariant = nameWithoutExtension.replace("@${variantPreset.name}", "") @@ -176,9 +182,11 @@ class EtherealGambi(val config: EtherealGambiConfig) { fileNameWithoutTheVariant ) } else { + val fileNameWithoutTheVariant = nameWithoutExtension.replace(GetFileRoute.variantRegex, "") + return VariantFound( DefaultImageVariant(imageType), - nameWithoutExtension + fileNameWithoutTheVariant ) } } @@ -204,6 +212,7 @@ class EtherealGambi(val config: EtherealGambiConfig) { it.key to when (it.value.imageType) { ImageType.PNG -> PNGImageInfo(this, it.value) ImageType.GIF -> GIFImageInfo(this, it.value) + ImageType.JPEG -> JPEGImageInfo(this, it.value) } } .toMap() @@ -237,4 +246,5 @@ class EtherealGambi(val config: EtherealGambiConfig) { val variant: ImageVariant, val nameWithoutExtension: String, ) : VariantResult() + data class OriginalFile(val path: String) : VariantResult() } \ No newline at end of file diff --git a/backend/src/main/kotlin/net/perfectdreams/etherealgambi/backend/routes/GetFileRoute.kt b/backend/src/main/kotlin/net/perfectdreams/etherealgambi/backend/routes/GetFileRoute.kt index 4bb7cfe..b3dc799 100644 --- a/backend/src/main/kotlin/net/perfectdreams/etherealgambi/backend/routes/GetFileRoute.kt +++ b/backend/src/main/kotlin/net/perfectdreams/etherealgambi/backend/routes/GetFileRoute.kt @@ -6,6 +6,7 @@ import io.ktor.server.response.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import net.perfectdreams.etherealgambi.backend.EtherealGambi +import net.perfectdreams.etherealgambi.backend.utils.SimpleImageInfo import net.perfectdreams.sequins.ktor.BaseRoute import java.io.File @@ -28,44 +29,62 @@ class GetFileRoute(val m: EtherealGambi) : BaseRoute("/{file...}") { val fileWithUnknownVariant = completePath.last() val pathWithoutFile = completePath.dropLast(1) - val variantResult = m.getVariantFromFileName(fileWithUnknownVariant) + val variantResult = m.getVariantFromFileName(pathWithoutFile.joinToString("/"), fileWithUnknownVariant) - when (variantResult) { - is EtherealGambi.UnsupportedVariantImageFormat -> { - // Couldn't figure out an image from the URL... so let's check if we can serve the file "raw"! - val file = File(m.files, completePath.joinToString("/")) - if (file.exists()) - call.respondBytes(file.readBytes()) - else - call.respondText("File not found", status = HttpStatusCode.NotFound) - return - } - is EtherealGambi.VariantNotFound -> { - call.respondText("Unknown Variant", status = HttpStatusCode.BadRequest) - return - } - is EtherealGambi.VariantFound -> { - val (variant, fileNameWithoutVariant) = variantResult + when (variantResult) { + is EtherealGambi.UnsupportedVariantImageFormat -> { + // Couldn't figure out an image from the URL... so let's check if we can serve the file "raw"! + val file = File(m.files, completePath.joinToString("/")) + if (file.exists()) + call.respondBytes(file.readBytes()) + else + call.respondText("File not found", status = HttpStatusCode.NotFound) + return + } + is EtherealGambi.VariantNotFound -> { + call.respondText("Unknown Variant", status = HttpStatusCode.BadRequest) + return + } + is EtherealGambi.VariantFound -> { + val (variant, fileNameWithoutVariant) = variantResult - val path = (pathWithoutFile + fileNameWithoutVariant).joinToString("/") + val path = (pathWithoutFile + fileNameWithoutVariant).joinToString("/") - withContext(Dispatchers.IO) { - val imageInfo = m.createImageInfoForImage(path) + withContext(Dispatchers.IO) { + val imageInfo = m.createImageInfoForImage(path) - if (imageInfo == null) { - call.respondText("Image not found", status = HttpStatusCode.NotFound) - return@withContext - } + if (imageInfo == null) { + call.respondText("Image not found", status = HttpStatusCode.NotFound) + return@withContext + } - val file = imageInfo.createImageVariant(variant) + val file = imageInfo.createImageVariant(variant) - call.respondBytes( - file.readBytes(), - variant.imageType.contentType - ) - } + call.respondBytes( + file.readBytes(), + variant.imageType.contentType + ) } } + + // Gets the original file + is EtherealGambi.OriginalFile -> { + val path = variantResult.path + + val image = File(m.files, path).readBytes() + val simpleImageInfo = SimpleImageInfo(image) + + call.respondBytes( + image, + contentType = when (simpleImageInfo.mimeType) { + "image/png" -> ContentType.Image.PNG + "image/gif" -> ContentType.Image.GIF + "image/jpeg" -> ContentType.Image.JPEG + else -> error("Unsupported mime type ${simpleImageInfo.mimeType}") + } + ) + } + } } catch (e: Exception) { e.printStackTrace() } diff --git a/backend/src/main/kotlin/net/perfectdreams/etherealgambi/backend/utils/JPEGImageInfo.kt b/backend/src/main/kotlin/net/perfectdreams/etherealgambi/backend/utils/JPEGImageInfo.kt new file mode 100644 index 0000000..d861a3e --- /dev/null +++ b/backend/src/main/kotlin/net/perfectdreams/etherealgambi/backend/utils/JPEGImageInfo.kt @@ -0,0 +1,61 @@ +package net.perfectdreams.etherealgambi.backend.utils + +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import net.perfectdreams.etherealgambi.backend.EtherealGambi +import net.perfectdreams.etherealgambi.data.DefaultImageVariant +import net.perfectdreams.etherealgambi.data.DefaultImageVariantPreset +import net.perfectdreams.etherealgambi.data.ImageInfoData +import net.perfectdreams.etherealgambi.data.ImageType +import net.perfectdreams.etherealgambi.data.ImageVariant +import net.perfectdreams.etherealgambi.data.ImageVariantInfoData +import net.perfectdreams.etherealgambi.data.ScaleDownToWidthImageVariant +import java.io.File + +class JPEGImageInfo( + m: EtherealGambi, + data: ImageInfoData +) : ImageInfo(m, data) { + override fun getValidVariantPresets() = listOf(DefaultImageVariantPreset) + + override suspend fun createImageVariant( + variant: ImageVariant + ): File { + return mutexes.getOrPut(variant) { Mutex() }.withLock { + val storedVariant = data.variants.firstOrNull { it.variantAttributes == variant } + + if (storedVariant != null) { + val storedVariantFile = File(m.generatedFiles, storedVariant.path) + if (storedVariant.optimizationVersion == EtherealGambi.OPTIMIZATION_VERSION && storedVariantFile.exists()) + return@withLock storedVariantFile + + data.variants.removeIf { it.variantAttributes == variant } + } + + val originalImageFile = File(m.files, path) + val generatedImageFile = File(m.generatedFiles, "$folder/${fileName.name}${variant.variantWithPrefix()}.${variant.imageType.extension}") + + when (variant) { + is DefaultImageVariant -> { + generatedImageFile.parentFile.mkdirs() + + generatedImageFile.writeBytes(originalImageFile.readBytes()) + + // TODO: Use jpegoptim to optimize the GIF! + + data.variants.add( + ImageVariantInfoData( + EtherealGambi.OPTIMIZATION_VERSION, + path, + variant, + generatedImageFile.length() + ) + ) + + return@withLock generatedImageFile + } + is ScaleDownToWidthImageVariant -> error("JPEG does not support scale down variations yet!") + } + } + } +} \ No newline at end of file