1515 */
1616package 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
1822import io.ktor.client.*
1923import io.ktor.client.request.*
2024import io.ktor.client.statement.*
@@ -23,8 +27,12 @@ import io.ktor.server.application.*
2327import io.ktor.server.request.*
2428import io.ktor.server.response.*
2529import 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
2937private const val SERVER_BANNER = " Ashampoo Image Proxy Service"
3038private const val AUTHORIZATION_HEADER = " Authorization"
@@ -34,8 +42,13 @@ private val validQualityRange = 10..100
3442private const val DEFAULT_LONG_SIDE_PX = 480
3543private const val DEFAULT_QUALITY = 90
3644
45+ private const val DEFAULT_TARGET_FORMAT = " .jpg"
46+
3747private val httpClient = HttpClient ()
3848
49+ private val noRotate = VipsOption .Enum (" no_rotate" , 1 )
50+ private val stripMetadata = VipsOption .Enum (" strip" , 1 )
51+
3952private 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}
0 commit comments