DEV Community

Cover image for HEIC to JPEG on macOS in Rust — Using sips Without Reinventing the Wheel
hiyoyo
hiyoyo

Posted on

HEIC to JPEG on macOS in Rust — Using sips Without Reinventing the Wheel

All tests run on an 8-year-old MacBook Air. All results from shipping 7 Mac apps as a solo developer. No sponsored opinion.

iPhones shoot HEIC. Most apps want JPEG. HiyokoAutoSync converts automatically during sync. The solution is already on every Mac: sips.


What sips is

sips (Scriptable Image Processing System) is a macOS built-in command-line tool for image conversion. It ships with every Mac, handles HEIC natively, and requires zero bundling.

sips -s format jpeg photo.heic --out photo.jpg
Enter fullscreen mode Exit fullscreen mode

One command. No dependencies. Works on Intel and Apple Silicon.


Calling sips from Rust

use std::process::Command;

pub async fn heic_to_jpeg(
    input: &Path,
    output: &Path,
) -> Result<(), AppError> {
    let status = Command::new("sips")
        .args([
            "-s", "format", "jpeg",
            "-s", "formatOptions", "85", // quality 0-100
            input.to_str().unwrap(),
            "--out",
            output.to_str().unwrap(),
        ])
        .status()?;

    if !status.success() {
        return Err(AppError::Conversion(
            format!("sips failed for {:?}", input)
        ));
    }

    Ok(())
}
Enter fullscreen mode Exit fullscreen mode

formatOptions sets JPEG quality. 85 is a good default — smaller than 100, visually indistinguishable for most photos.


Async conversion with Tauri

Command::new is blocking. Wrap in spawn_blocking for use in async Tauri commands:

let input = input_path.clone();
let output = output_path.clone();

tokio::task::spawn_blocking(move || {
    heic_to_jpeg_sync(&input, &output)
}).await??;
Enter fullscreen mode Exit fullscreen mode

Batch conversion

For parallel HEIC conversion during sync, the same semaphore pattern applies:

let semaphore = Arc::new(Semaphore::new(4)); // 4 concurrent conversions

for heic_file in heic_files {
    let sem = Arc::clone(&semaphore);
    tokio::spawn(async move {
        let _permit = sem.acquire().await.unwrap();
        heic_to_jpeg(&heic_file, &jpeg_output(&heic_file)).await
    });
}
Enter fullscreen mode Exit fullscreen mode

4 concurrent sips processes is comfortable on most Macs. More than 6 and you'll see diminishing returns.


The alternative: pure Rust

There are Rust crates for HEIC decoding. They require bundling native libraries, add significant binary size, and don't handle all HEIC variants as well as Apple's own implementation.

For a macOS-only app, sips is the correct choice. Use the platform.


TL;DR: For HEIC→JPEG on macOS, skip the Rust crates and use the built-in sips command. Call it via Command::new, wrap in spawn_blocking for async Tauri commands, and use a Semaphore with 4 concurrent processes for batch conversion. Zero dependencies, Apple-quality output.


If this was useful, a ❤️ helps more than you'd think — thanks!

HiyokoAutoSync | X → @hiyoyok

Top comments (0)