HEIC (High Efficiency Image Container) is Apple's default photo format since iOS 11. It stores images with H.265 (HEVC) compression — more efficient than JPEG but requiring a native decoder. Java has no JDK support for HEIC. The practical options are: wrap a native decoder (ImageMagick, libheif, or the system's heif-convert) via ProcessBuilder, or offload to the ChangeThisFile API which runs libheif server-side. No pure-Java HEIC decoder currently handles the full spec reliably.

Method 1: ImageMagick via ProcessBuilder (libheif backend)

ImageMagick's convert command uses libheif to decode HEIC. If ImageMagick is installed on your server, this is the simplest approach.

# Ubuntu/Debian — installs ImageMagick with HEIC support
sudo apt install imagemagick libheif-dev

# macOS
brew install imagemagick
import java.io.IOException;
import java.nio.file.Path;
import java.util.concurrent.TimeUnit;

public class HeicToJpgImageMagick {

    public static void convert(Path heicPath, Path jpgPath, int quality)
            throws IOException, InterruptedException {

        ProcessBuilder pb = new ProcessBuilder(
            "convert",
            "-quality", String.valueOf(quality),   // JPEG quality 1-100
            heicPath.toAbsolutePath().toString(),
            jpgPath.toAbsolutePath().toString()
        );
        pb.redirectErrorStream(true);

        Process process = pb.start();
        String output = new String(process.getInputStream().readAllBytes());
        boolean finished = process.waitFor(60, TimeUnit.SECONDS);

        if (!finished) {
            process.destroyForcibly();
            throw new IOException("ImageMagick timed out");
        }
        if (process.exitValue() != 0) {
            throw new IOException("ImageMagick failed: " + output);
        }
    }

    public static void main(String[] args) throws Exception {
        convert(Path.of("photo.heic"), Path.of("photo.jpg"), 85);
        System.out.println("Converted to photo.jpg");
    }
}

On some Ubuntu configurations, ImageMagick's security policy blocks HEIC by default. Edit /etc/ImageMagick-6/policy.xml and change the HEIC policy from none to read|write:

<policy domain="coder" rights="read|write" pattern="HEIC" />

libheif heif-convert (lightweight alternative to ImageMagick)

If you don't need the full ImageMagick toolchain, heif-convert from libheif is a smaller dependency:

# Ubuntu/Debian
sudo apt install libheif-examples  # Installs heif-convert
import java.io.IOException;
import java.nio.file.Path;
import java.util.concurrent.TimeUnit;

public class HeicToJpgLibheif {

    public static void convert(Path heicPath, Path jpgPath)
            throws IOException, InterruptedException {

        ProcessBuilder pb = new ProcessBuilder(
            "heif-convert",
            heicPath.toAbsolutePath().toString(),
            jpgPath.toAbsolutePath().toString()
        );
        pb.redirectErrorStream(true);

        Process process = pb.start();
        String output = new String(process.getInputStream().readAllBytes());
        boolean finished = process.waitFor(60, TimeUnit.SECONDS);

        if (!finished) {
            process.destroyForcibly();
            throw new IOException("heif-convert timed out");
        }
        if (process.exitValue() != 0) {
            throw new IOException("heif-convert failed: " + output);
        }
    }

    public static void main(String[] args) throws Exception {
        convert(Path.of("photo.heic"), Path.of("photo.jpg"));
        System.out.println("Converted to photo.jpg");
    }
}

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

For cloud deployments without native libraries, the API runs libheif server-side. No system dependencies needed in your Java environment. 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 HeicToJpgApi {

    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 heicPath) throws IOException, InterruptedException {
        String boundary = "----CTFBoundary" + UUID.randomUUID().toString().replace("-", "");
        byte[] fileBytes = Files.readAllBytes(heicPath);

        List<byte[]> parts = new ArrayList<>();
        parts.add(("--" + boundary + "\r\n" +
            "Content-Disposition: form-data; name=\"target\"\r\n\r\njpg\r\n").getBytes(StandardCharsets.UTF_8));
        parts.add(("--" + boundary + "\r\n" +
            "Content-Disposition: form-data; name=\"file\"; filename=\"" + heicPath.getFileName() + "\"\r\n" +
            "Content-Type: image/heic\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(60))
            .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[] jpg = convert(Path.of("photo.heic"));
        Files.write(Path.of("photo.jpg"), jpg);
        System.out.println("Saved photo.jpg (" + jpg.length + " bytes)");
    }
}

The API accepts both .heic and .heif extensions. Source format is auto-detected from file content — the extension doesn't need to match.

When to use each

ApproachBest forTradeoff
ImageMagick (convert)Full control over quality/resize/color, rich toolchain~50MB install; may need policy.xml edit for HEIC
heif-convertLightweight, single-purpose HEIC decoderLess control than ImageMagick; fewer output options
ChangeThisFile APIZero native deps, Lambda/Cloud Run, container deploymentsNetwork latency; 25MB limit on free tier (HEIC files are compact)

Production tips

  • Use CompletableFuture for batch HEIC processing. Each ProcessBuilder call is independent — parallelize across a thread pool sized to available CPUs. HEIC decoding via libheif is CPU-bound.
  • Preserve EXIF metadata when converting. ImageMagick preserves EXIF by default. If metadata matters (GPS, camera model, orientation), verify with exiftool after conversion. The ChangeThisFile API preserves basic EXIF data.
  • HEIC files from iPhone burst photos contain multiple images. A HEIC burst produces a multi-image HEIC container. heif-convert extracts all images as numbered JPGs. ImageMagick's convert extracts the primary image only by default — add [0] to the input path to be explicit: photo.heic[0].
  • Check ImageMagick security policy on fresh installs. Ubuntu's default ImageMagick policy blocks many formats including HEIC. Edit /etc/ImageMagick-6/policy.xml before your first test.
  • API calls for HEIC are fast. HEIC files from iPhone are typically 1–4MB (much smaller than equivalent JPEG). API calls with these sizes complete in under 5 seconds, well within the default 60-second timeout.

HEIC decoding in Java requires native code — there's no pure-Java option that handles the full spec. For servers you control, ImageMagick or heif-convert via ProcessBuilder works reliably. For containerized or serverless Java apps, the ChangeThisFile API eliminates native dependencies entirely. Free tier covers 1,000 conversions/month.