Java's standard library (javax.imageio.ImageIO) reads JPG and PNG natively but has no WebP encoder — WebP support was never added to the JDK. The TwelveMonkeys ImageIO library fills this gap: add the JAR and WebP writes through the standard ImageIO.write() API. If you'd rather keep your classpath clean, the ChangeThisFile API accepts any image format and returns WebP from a HttpClient POST.

Method 1: TwelveMonkeys ImageIO (native Java WebP writer)

TwelveMonkeys ImageIO extends javax.imageio via the service provider mechanism — add the JARs and WebP becomes a first-class format in the standard API.

<dependency>
  <groupId>com.twelvemonkeys.imageio</groupId>
  <artifactId>imageio-webp</artifactId>
  <version>3.11.0</version>
</dependency>
<!-- Core is a transitive dep but listing it explicitly avoids surprises -->
<dependency>
  <groupId>com.twelvemonkeys.imageio</groupId>
  <artifactId>imageio-core</artifactId>
  <version>3.11.0</version>
</dependency>
import com.twelvemonkeys.imageio.plugins.webp.WebPImageWriterSpi;

import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.stream.ImageOutputStream;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;

public class JpgToWebp {

    /**
     * Convert a JPG to WebP with configurable quality (0.0 to 1.0).
     * quality = 1.0 → lossless; quality < 1.0 → lossy WebP
     */
    public static void convert(Path jpgPath, Path webpPath, float quality) throws IOException {
        BufferedImage img = ImageIO.read(jpgPath.toFile());
        if (img == null) throw new IOException("Cannot read image: " + jpgPath);

        ImageWriter writer = ImageIO.getImageWritersByMIMEType("image/webp").next();
        ImageWriteParam param = writer.getDefaultWriteParam();

        if (param.canWriteCompressed()) {
            param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
            param.setCompressionType(quality >= 1.0f ? "Lossless" : "Lossy");
            if (quality < 1.0f) {
                param.setCompressionQuality(quality);
            }
        }

        try (ImageOutputStream ios = ImageIO.createImageOutputStream(webpPath.toFile())) {
            writer.setOutput(ios);
            writer.write(null, new IIOImage(img, null, null), param);
        } finally {
            writer.dispose();
        }
    }

    public static void main(String[] args) throws IOException {
        // Lossy WebP at 80% quality — ~30% smaller than the source JPG
        convert(Path.of("photo.jpg"), Path.of("photo.webp"), 0.80f);
        System.out.println("Converted to WebP");
    }
}

TwelveMonkeys uses libwebp internally for encoding. The quality parameter maps directly to libwebp's quality setting. A value of 0.80–0.85 is a good default — visually equivalent to a high-quality JPEG at roughly 30% smaller file size.

For lossless WebP (exact pixel preservation), pass quality = 1.0f and the compression type switches to "Lossless" automatically in the code above.

Method 2: ChangeThisFile API (Java 11 HttpClient, no SDK)

Send the JPG as a multipart POST to the API using Java 11's built-in HttpClient. No extra dependencies needed. Free tier covers 1,000 conversions/month.

import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

public class JpgToWebpApi {

    private static final String API_KEY = "ctf_sk_your_key_here";
    private static final String API_URL = "https://changethisfile.com/v1/convert";
    private static final HttpClient HTTP = HttpClient.newBuilder()
        .connectTimeout(Duration.ofSeconds(10))
        .build();

    public static byte[] convert(Path jpgPath) throws IOException, InterruptedException {
        String boundary = "----CTFBoundary" + UUID.randomUUID().toString().replace("-", "");
        byte[] fileBytes = Files.readAllBytes(jpgPath);

        List<byte[]> parts = new ArrayList<>();
        parts.add(("--" + boundary + "\r\n" +
            "Content-Disposition: form-data; name=\"target\"\r\n\r\nwebp\r\n").getBytes(StandardCharsets.UTF_8));
        parts.add(("--" + boundary + "\r\n" +
            "Content-Disposition: form-data; name=\"file\"; filename=\"" + jpgPath.getFileName() + "\"\r\n" +
            "Content-Type: image/jpeg\r\n\r\n").getBytes(StandardCharsets.UTF_8));
        parts.add(fileBytes);
        parts.add(("\r\n--" + boundary + "--\r\n").getBytes(StandardCharsets.UTF_8));

        int totalLen = parts.stream().mapToInt(b -> b.length).sum();
        byte[] body = new byte[totalLen];
        int offset = 0;
        for (byte[] part : parts) {
            System.arraycopy(part, 0, body, offset, part.length);
            offset += part.length;
        }

        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create(API_URL))
            .header("Authorization", "Bearer " + API_KEY)
            .header("Content-Type", "multipart/form-data; boundary=" + boundary)
            .timeout(Duration.ofSeconds(30))
            .POST(HttpRequest.BodyPublishers.ofByteArray(body))
            .build();

        HttpResponse<byte[]> response = HTTP.send(request,
            HttpResponse.BodyHandlers.ofByteArray());

        if (response.statusCode() != 200) {
            throw new IOException("API error " + response.statusCode() +
                ": " + new String(response.body()));
        }
        return response.body();
    }

    public static void main(String[] args) throws Exception {
        byte[] webp = convert(Path.of("photo.jpg"));
        Files.write(Path.of("photo.webp"), webp);
        System.out.println("Saved photo.webp (" + webp.length + " bytes)");
    }
}

Batch conversion with CompletableFuture

For converting a directory of JPGs, parallelize with CompletableFuture:

import java.io.IOException;
import java.nio.file.*;
import java.util.List;
import java.util.concurrent.*;
import java.util.stream.*;

public class BatchJpgToWebp {

    public static void convertDirectory(Path inputDir, Path outputDir) throws Exception {
        Files.createDirectories(outputDir);
        List<Path> jpgs;
        try (var stream = Files.walk(inputDir)) {
            jpgs = stream.filter(p -> p.toString().toLowerCase().endsWith(".jpg")
                || p.toString().toLowerCase().endsWith(".jpeg"))
                .collect(Collectors.toList());
        }

        ExecutorService pool = Executors.newFixedThreadPool(
            Runtime.getRuntime().availableProcessors());

        List<CompletableFuture<Void>> futures = jpgs.stream()
            .map(jpg -> CompletableFuture.runAsync(() -> {
                Path outPath = outputDir.resolve(
                    jpg.getFileName().toString().replaceAll("(?i)\\.jpe?g$", ".webp"));
                try {
                    JpgToWebp.convert(jpg, outPath, 0.80f);
                    System.out.println("Converted: " + outPath.getFileName());
                } catch (IOException e) {
                    System.err.println("Failed: " + jpg + ": " + e.getMessage());
                }
            }, pool))
            .collect(Collectors.toList());

        CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
        pool.shutdown();
        System.out.println("Batch complete: " + futures.size() + " files");
    }
}

When to use each

ApproachBest forTradeoff
TwelveMonkeys ImageIOIn-process Java encoding, lossless/lossy control, batch jobsExtra JAR dependency; libwebp bundled (~2MB)
ChangeThisFile APIZero deps, Lambda/Cloud Run, one-off conversionsNetwork latency; 25MB limit on free tier

Production tips

  • Quality 0.80–0.85 is the sweet spot. WebP at 80% quality is visually indistinguishable from a high-quality JPEG for most photographs, at 25–35% smaller file size.
  • Use lossless WebP for graphics, lossy for photos. Screenshots, logos, and UI elements with flat colors compress much better with lossless. Photos with gradients and noise are better served by lossy.
  • Parallelize with a thread pool sized to CPU cores. Image encoding is CPU-bound. Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()) gives you full CPU utilization without oversubscribing.
  • Don't convert WebP back to JPG. Each lossy compression cycle degrades quality. If you need to edit and re-export, work from the original JPG source.
  • Reuse HttpClient for API calls. One instance per JVM handles connection pooling automatically. Creating a new client per request is expensive (TLS handshake + connection establishment).

TwelveMonkeys is the cleanest in-process option — it slots into the standard ImageIO API and gives you fine quality control. For serverless environments or when you want zero extra JARs, the ChangeThisFile API handles WebP encoding with Java 11's built-in HttpClient. Free tier covers 1,000 conversions/month.