Skip to content

Commit 8f4d314

Browse files
committed
Use libvips
1 parent 1452eb4 commit 8f4d314

File tree

9 files changed

+119
-131
lines changed

9 files changed

+119
-131
lines changed

.run/HTTP test.run.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<component name="ProjectRunConfigurationManager">
2+
<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">
3+
<method v="2" />
4+
</configuration>
5+
</component>

Dockerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ COPY src src
77
RUN ./gradlew --no-daemon --info buildFatJar
88

99
FROM amazoncorretto:22-alpine
10+
RUN apk add --no-cache vips-dev
1011
RUN mkdir /app
1112
COPY --from=BUILD_STAGE /tmp/build/libs/*-all.jar /app/ktor-server.jar
1213
EXPOSE 8080:8080

README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,21 @@
11
# Ashampoo Image Proxy Service
2+
3+
## Installation
4+
5+
WiP
6+
7+
## Contributions
8+
9+
Contributions are welcome! If you encounter any issues,
10+
have suggestions for improvements, or would like to contribute new features,
11+
please feel free to submit a pull request.
12+
13+
## Acknowledgements
14+
15+
* JetBrains for making [Kotlin](https://kotlinlang.org).
16+
* John Cupitt for making [libvips](https://github.com/libvips/).
17+
* carrot for making [vips-ffm](https://github.com/lopcode/vips-ffm).
18+
19+
## License
20+
21+
This code is under the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0).

Test.http

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
GET http://0.0.0.0:8080/
2+
RemoteUrl: https://raw.githubusercontent.com/Ashampoo/imageproxy/refs/heads/main/src/main/resources/sample.jpg
3+
LongSidePx: 320
4+
Quality: 80
5+

build.gradle.kts

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
val kotlin_version: String by project
22
val logback_version: String by project
33
val vips_ffm_version: String by project
4-
val skiko_version: String by project
54

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

4645
implementation("ch.qos.logback:logback-classic:$logback_version")
47-
48-
/*
49-
* SKIKO
50-
*/
51-
52-
val osName = System.getProperty("os.name")
53-
val targetOs = when {
54-
osName == "Mac OS X" -> "macos"
55-
osName.startsWith("Win") -> "windows"
56-
osName.startsWith("Linux") -> "linux"
57-
else -> error("Unsupported OS: $osName")
58-
}
59-
60-
val osArch = System.getProperty("os.arch")
61-
val targetArch = when (osArch) {
62-
"x86_64", "amd64" -> "x64"
63-
"aarch64" -> "arm64"
64-
else -> error("Unsupported arch: $osArch")
65-
}
66-
67-
val target = "${targetOs}-${targetArch}"
68-
dependencies {
69-
implementation("org.jetbrains.skiko:skiko-awt-runtime-$target:$skiko_version")
70-
}
7146
}

gradle.properties

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,3 @@ kotlin_version=2.1.0
33
ktor_version=3.0.2
44
logback_version=1.4.14
55
vips_ffm_version=1.3.0
6-
skiko_version=0.8.18

src/main/kotlin/com/ashampoo/imageproxy/Application.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,16 @@
1515
*/
1616
package com.ashampoo.imageproxy
1717

18+
import app.photofox.vipsffm.Vips
1819
import io.ktor.server.application.*
1920
import io.ktor.server.engine.*
2021
import io.ktor.server.netty.*
2122

2223
fun main() {
24+
25+
/* Initialize LibVips */
26+
Vips.init()
27+
2328
embeddedServer(
2429
factory = Netty,
2530
port = 8080,

src/main/kotlin/com/ashampoo/imageproxy/Routing.kt

Lines changed: 83 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515
*/
1616
package com.ashampoo.imageproxy
1717

18+
import app.photofox.vipsffm.VImage
19+
import app.photofox.vipsffm.Vips
20+
import app.photofox.vipsffm.VipsError
21+
import app.photofox.vipsffm.VipsOption
1822
import io.ktor.client.*
1923
import io.ktor.client.request.*
2024
import io.ktor.client.statement.*
@@ -23,8 +27,12 @@ import io.ktor.server.application.*
2327
import io.ktor.server.request.*
2428
import io.ktor.server.response.*
2529
import io.ktor.server.routing.*
26-
import org.jetbrains.skia.Image
27-
import org.slf4j.LoggerFactory
30+
import kotlinx.coroutines.CompletableDeferred
31+
import kotlinx.coroutines.Dispatchers
32+
import kotlinx.coroutines.withContext
33+
import java.io.ByteArrayOutputStream
34+
import kotlin.math.max
35+
import kotlin.math.round
2836

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

45+
private const val DEFAULT_TARGET_FORMAT = ".jpg"
46+
3747
private val httpClient = HttpClient()
3848

49+
private val noRotate = VipsOption.Enum("no_rotate", 1)
50+
private val stripMetadata = VipsOption.Enum("strip", 1)
51+
3952
private val usageString = buildString {
4053

4154
appendLine(SERVER_BANNER)
@@ -104,17 +117,77 @@ fun Application.configureRouting() {
104117

105118
val remoteBytes = response.bodyAsBytes()
106119

107-
val image = Image.makeFromEncoded(remoteBytes)
120+
try {
121+
122+
val thumbnailBytes = createThumbnailBytes(
123+
originalBytes = remoteBytes,
124+
longSidePx = longSidePx,
125+
quality = quality
126+
)
127+
128+
call.respondBytes(
129+
bytes = thumbnailBytes,
130+
contentType = ContentType.Image.JPEG,
131+
status = HttpStatusCode.OK
132+
)
133+
134+
} catch (ex: VipsError) {
135+
136+
log.error("Error in image processing.", ex)
137+
138+
call.respond(HttpStatusCode.InternalServerError, ex.message ?: "Error")
139+
140+
return@get
141+
}
142+
}
143+
}
144+
}
145+
146+
private suspend fun createThumbnailBytes(
147+
originalBytes: ByteArray,
148+
longSidePx: Int,
149+
quality: Int
150+
): ByteArray {
151+
152+
val deferred = CompletableDeferred<ByteArray>()
153+
154+
withContext(Dispatchers.IO) {
155+
156+
try {
108157

109-
val thumbnail = image.scale(longSidePx)
158+
Vips.run { arena ->
110159

111-
val thumbnailBytes = thumbnail.encodeToJpg(quality)
160+
val sourceImage = VImage.newFromBytes(arena, originalBytes)
161+
162+
val resizeFactor: Double =
163+
longSidePx / max(sourceImage.width.toDouble(), sourceImage.height.toDouble())
164+
165+
@Suppress("MagicNumber")
166+
val thumbnailWidth = max(1, round(resizeFactor * sourceImage.width + 0.3).toInt())
167+
168+
val thumbnail = sourceImage.thumbnailImage(
169+
thumbnailWidth,
170+
noRotate
171+
)
112172

113-
call.respondBytes(
114-
bytes = thumbnailBytes,
115-
contentType = ContentType.Image.JPEG,
116-
status = HttpStatusCode.OK
117-
)
173+
val outputStream = ByteArrayOutputStream()
174+
175+
thumbnail.writeToStream(
176+
outputStream,
177+
DEFAULT_TARGET_FORMAT,
178+
stripMetadata,
179+
VipsOption.Enum("Q", quality)
180+
)
181+
182+
val thumbnailBytes = outputStream.toByteArray()
183+
184+
deferred.complete(thumbnailBytes)
185+
}
186+
187+
} catch (ex: Exception) {
188+
deferred.completeExceptionally(ex)
118189
}
119190
}
191+
192+
return deferred.await()
120193
}

src/main/kotlin/com/ashampoo/imageproxy/SkikoImageUtils.kt

Lines changed: 0 additions & 95 deletions
This file was deleted.

0 commit comments

Comments
 (0)