Skip to content

Commit

Permalink
Use libvips
Browse files Browse the repository at this point in the history
  • Loading branch information
StefanOltmann committed Dec 13, 2024
1 parent 1452eb4 commit 8f4d314
Show file tree
Hide file tree
Showing 9 changed files with 119 additions and 131 deletions.
5 changes: 5 additions & 0 deletions .run/HTTP test.run.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="HTTP test" type="HttpClient.HttpRequestRunConfigurationType" factoryName="HTTP Request" path="$PROJECT_DIR$/Test.http" requestIdentifier="GET request to example server" runType="Run single request">
<method v="2" />
</configuration>
</component>
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ COPY src src
RUN ./gradlew --no-daemon --info buildFatJar

FROM amazoncorretto:22-alpine
RUN apk add --no-cache vips-dev
RUN mkdir /app
COPY --from=BUILD_STAGE /tmp/build/libs/*-all.jar /app/ktor-server.jar
EXPOSE 8080:8080
Expand Down
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,21 @@
# Ashampoo Image Proxy Service

## Installation

WiP

## Contributions

Contributions are welcome! If you encounter any issues,
have suggestions for improvements, or would like to contribute new features,
please feel free to submit a pull request.

## Acknowledgements

* JetBrains for making [Kotlin](https://kotlinlang.org).
* John Cupitt for making [libvips](https://github.com/libvips/).
* carrot for making [vips-ffm](https://github.com/lopcode/vips-ffm).

## License

This code is under the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0).
5 changes: 5 additions & 0 deletions Test.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
GET http://0.0.0.0:8080/
RemoteUrl: https://raw.githubusercontent.com/Ashampoo/imageproxy/refs/heads/main/src/main/resources/sample.jpg
LongSidePx: 320
Quality: 80

25 changes: 0 additions & 25 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
val kotlin_version: String by project
val logback_version: String by project
val vips_ffm_version: String by project
val skiko_version: String by project

plugins {
kotlin("jvm") version "2.1.0"
Expand Down Expand Up @@ -44,28 +43,4 @@ dependencies {
implementation("app.photofox.vips-ffm:vips-ffm-core:$vips_ffm_version")

implementation("ch.qos.logback:logback-classic:$logback_version")

/*
* SKIKO
*/

val osName = System.getProperty("os.name")
val targetOs = when {
osName == "Mac OS X" -> "macos"
osName.startsWith("Win") -> "windows"
osName.startsWith("Linux") -> "linux"
else -> error("Unsupported OS: $osName")
}

val osArch = System.getProperty("os.arch")
val targetArch = when (osArch) {
"x86_64", "amd64" -> "x64"
"aarch64" -> "arm64"
else -> error("Unsupported arch: $osArch")
}

val target = "${targetOs}-${targetArch}"
dependencies {
implementation("org.jetbrains.skiko:skiko-awt-runtime-$target:$skiko_version")
}
}
1 change: 0 additions & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,3 @@ kotlin_version=2.1.0
ktor_version=3.0.2
logback_version=1.4.14
vips_ffm_version=1.3.0
skiko_version=0.8.18
5 changes: 5 additions & 0 deletions src/main/kotlin/com/ashampoo/imageproxy/Application.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,16 @@
*/
package com.ashampoo.imageproxy

import app.photofox.vipsffm.Vips
import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*

fun main() {

/* Initialize LibVips */
Vips.init()

embeddedServer(
factory = Netty,
port = 8080,
Expand Down
93 changes: 83 additions & 10 deletions src/main/kotlin/com/ashampoo/imageproxy/Routing.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
*/
package com.ashampoo.imageproxy

import app.photofox.vipsffm.VImage
import app.photofox.vipsffm.Vips
import app.photofox.vipsffm.VipsError
import app.photofox.vipsffm.VipsOption
import io.ktor.client.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
Expand All @@ -23,8 +27,12 @@ import io.ktor.server.application.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import org.jetbrains.skia.Image
import org.slf4j.LoggerFactory
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.ByteArrayOutputStream
import kotlin.math.max
import kotlin.math.round

private const val SERVER_BANNER = "Ashampoo Image Proxy Service"
private const val AUTHORIZATION_HEADER = "Authorization"
Expand All @@ -34,8 +42,13 @@ private val validQualityRange = 10..100
private const val DEFAULT_LONG_SIDE_PX = 480
private const val DEFAULT_QUALITY = 90

private const val DEFAULT_TARGET_FORMAT = ".jpg"

private val httpClient = HttpClient()

private val noRotate = VipsOption.Enum("no_rotate", 1)
private val stripMetadata = VipsOption.Enum("strip", 1)

private val usageString = buildString {

appendLine(SERVER_BANNER)
Expand Down Expand Up @@ -104,17 +117,77 @@ fun Application.configureRouting() {

val remoteBytes = response.bodyAsBytes()

val image = Image.makeFromEncoded(remoteBytes)
try {

val thumbnailBytes = createThumbnailBytes(
originalBytes = remoteBytes,
longSidePx = longSidePx,
quality = quality
)

call.respondBytes(
bytes = thumbnailBytes,
contentType = ContentType.Image.JPEG,
status = HttpStatusCode.OK
)

} catch (ex: VipsError) {

log.error("Error in image processing.", ex)

call.respond(HttpStatusCode.InternalServerError, ex.message ?: "Error")

return@get
}
}
}
}

private suspend fun createThumbnailBytes(
originalBytes: ByteArray,
longSidePx: Int,
quality: Int
): ByteArray {

val deferred = CompletableDeferred<ByteArray>()

withContext(Dispatchers.IO) {

try {

val thumbnail = image.scale(longSidePx)
Vips.run { arena ->

val thumbnailBytes = thumbnail.encodeToJpg(quality)
val sourceImage = VImage.newFromBytes(arena, originalBytes)

val resizeFactor: Double =
longSidePx / max(sourceImage.width.toDouble(), sourceImage.height.toDouble())

@Suppress("MagicNumber")
val thumbnailWidth = max(1, round(resizeFactor * sourceImage.width + 0.3).toInt())

val thumbnail = sourceImage.thumbnailImage(
thumbnailWidth,
noRotate
)

call.respondBytes(
bytes = thumbnailBytes,
contentType = ContentType.Image.JPEG,
status = HttpStatusCode.OK
)
val outputStream = ByteArrayOutputStream()

thumbnail.writeToStream(
outputStream,
DEFAULT_TARGET_FORMAT,
stripMetadata,
VipsOption.Enum("Q", quality)
)

val thumbnailBytes = outputStream.toByteArray()

deferred.complete(thumbnailBytes)
}

} catch (ex: Exception) {
deferred.completeExceptionally(ex)
}
}

return deferred.await()
}
95 changes: 0 additions & 95 deletions src/main/kotlin/com/ashampoo/imageproxy/SkikoImageUtils.kt

This file was deleted.

0 comments on commit 8f4d314

Please sign in to comment.