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
| Approach | Best for | Tradeoff |
|---|---|---|
| ImageMagick (convert) | Full control over quality/resize/color, rich toolchain | ~50MB install; may need policy.xml edit for HEIC |
| heif-convert | Lightweight, single-purpose HEIC decoder | Less control than ImageMagick; fewer output options |
| ChangeThisFile API | Zero native deps, Lambda/Cloud Run, container deployments | Network 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
exiftoolafter 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-convertextracts all images as numbered JPGs. ImageMagick'sconvertextracts 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.xmlbefore 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.