JPG → WebP cuts file sizes by 25–35% at equivalent visual quality — a meaningful win for mobile apps and web performance. The JVM doesn't ship a WebP encoder out of the box, but the webp-imageio plugin plugs directly into ImageIO. Android has native WebP support since API 30. This guide covers both plus the ChangeThisFile API shortcut.

Method 1: ImageIO + webp-imageio plugin (JVM/server-side)

The webp-imageio library registers a WebP ImageWriter with the standard Java ImageIO framework. No native binaries — the encoder is bundled in the JAR.

// build.gradle.kts
dependencies {
    implementation("org.sejda.imageio:webp-imageio:0.1.6")
}
import com.luciad.imageio.webp.WebPWriteParam
import java.io.File
import javax.imageio.IIOImage
import javax.imageio.ImageIO
import javax.imageio.ImageWriteParam

fun jpgToWebp(inputPath: String, outputPath: String, quality: Float = 0.85f) {
    val image = ImageIO.read(File(inputPath))
        ?: error("Cannot read image: $inputPath")

    val writer = ImageIO.getImageWritersByMIMEType("image/webp").next()
    val param = writer.defaultWriteParam as WebPWriteParam
    param.compressionMode = ImageWriteParam.MODE_EXPLICIT
    param.compressionType = "Lossy"
    param.compressionQuality = quality

    val outFile = File(outputPath)
    ImageIO.createImageOutputStream(outFile).use { stream ->
        writer.output = stream
        writer.write(null, IIOImage(image, null, null), param)
    }
    writer.dispose()
    println("Saved: $outputPath (${outFile.length() / 1024} KB)")
}

fun main() {
    jpgToWebp("photo.jpg", "photo.webp", quality = 0.85f)
}

Quality guide:

  • 0.60–0.70 — Aggressive compression. Visible artifacts on gradients.
  • 0.80–0.85 — Good default. Indistinguishable from JPEG at same size.
  • 0.90+ — Near-lossless. Larger than JPEG at equivalent quality.

For lossless WebP (e.g., illustrations, screenshots), set compressionType = "Lossless" and omit compressionQuality.

Method 2: Android / Compose (BitmapFactory + Bitmap.compress)

On Android, the platform provides native WebP encoding since API 30 (Android 11). No extra dependencies needed.

import android.graphics.Bitmap
import android.graphics.BitmapFactory
import java.io.File
import java.io.FileOutputStream

fun jpgToWebpAndroid(
    inputPath: String,
    outputPath: String,
    quality: Int = 85
) {
    val bitmap = BitmapFactory.decodeFile(inputPath)
        ?: error("Cannot decode: $inputPath")

    FileOutputStream(outputPath).use { out ->
        // WEBP_LOSSY requires API 30+; use WEBP for API < 30 (always lossy)
        val format = if (android.os.Build.VERSION.SDK_INT >= 30) {
            Bitmap.CompressFormat.WEBP_LOSSY
        } else {
            @Suppress("DEPRECATION")
            Bitmap.CompressFormat.WEBP
        }
        bitmap.compress(format, quality, out)
    }
    bitmap.recycle()
    println("Saved: $outputPath")
}

For Compose, load with BitmapPainter(BitmapFactory.decodeFile(path).asImageBitmap()) for display, then compress with the above for saving. Android handles the encoder natively — no WebP plugin JAR needed.

API level note: WEBP_LOSSY and WEBP_LOSSLESS were added in API 30. For apps targeting API 18–29, fall back to Bitmap.CompressFormat.WEBP (deprecated but functional lossy encoder).

Method 3: ChangeThisFile API via Ktor HttpClient (no encoder deps)

No webp-imageio JAR, no Android API version concerns. POST the JPG to /v1/convert and receive WebP. Free tier: 1,000 conversions/month.

# curl reference
curl -X POST https://changethisfile.com/v1/convert \
  -H "Authorization: Bearer ctf_sk_your_key_here" \
  -F "file=@photo.jpg" \
  -F "target=webp" \
  --output photo.webp
import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.request.*
import io.ktor.client.request.forms.*
import io.ktor.client.statement.*
import io.ktor.http.*
import java.io.File

const val API_KEY = "ctf_sk_your_key_here"

suspend fun jpgToWebpApi(inputPath: String, outputPath: String) {
    val client = HttpClient(CIO)
    val response: HttpResponse = client.submitFormWithBinaryData(
        url = "https://changethisfile.com/v1/convert",
        formData = formData {
            append("file", File(inputPath).readBytes(), Headers.build {
                append(HttpHeaders.ContentDisposition, "filename=\"${File(inputPath).name}\"")
                append(HttpHeaders.ContentType, ContentType.Image.JPEG)
            })
            append("target", "webp")
        }
    ) {
        header("Authorization", "Bearer $API_KEY")
    }
    File(outputPath).writeBytes(response.readBytes())
    client.close()
    println("Saved: $outputPath")
}

fun main() = kotlinx.coroutines.runBlocking {
    jpgToWebpApi("photo.jpg", "photo.webp")
}

Source format is auto-detected from the file content. The API uses libvips for conversion — fast and memory-efficient for large images.

When to use each

ApproachBest forTradeoff
webp-imageio (JVM)Server-side batch conversion, any JVM platformExtra JAR dependency; no Android support
Bitmap.compress (Android)Android apps, Compose UIsAPI 30+ for WEBP_LOSSY; deprecated format on older APIs
ChangeThisFile API (Ktor)No encoder deps, edge functions, cross-platformNetwork call, 25MB free-tier upload limit

Production tips

  • Don't re-encode WebP from an already-compressed JPEG. JPEG artifacts bake in during re-encoding. If you have the original uncompressed source, encode from that. If JPEG is all you have, quality=0.85 is a reasonable floor.
  • Strip EXIF before encoding for web. JPEG EXIF data (GPS, camera model) doesn't carry over to WebP unless you explicitly copy it. For web use this is a feature — no accidental location data leak.
  • Batch with a thread pool, not a coroutine-per-file. Image encoding is CPU-bound. Use a fixed-thread Executor or Dispatchers.Default with a semaphore, not an unbounded coroutine launch per file.
  • Verify browser support if serving to older clients. WebP is supported in all modern browsers but not IE. If serving to enterprise environments, keep a JPEG fallback or use <picture> with WebP as the primary source.
  • Use lossless WebP for PNG-origin images. Lossless WebP is typically 20–30% smaller than PNG with zero quality loss. Don't compress PNGs as lossy WebP — it degrades them.

For JVM services, webp-imageio via ImageIO is the cleanest path. For Android, use the native Bitmap.compress API. For both cases where you want zero encoder dependencies, the ChangeThisFile API handles it server-side. Free tier covers 1,000 conversions/month.