Every iPhone and iPad captures photos in HEIC by default — Apple's efficient format that stores the same quality as JPG at roughly half the file size. The problem: most non-Apple tools (WordPress, Shopify, email clients, Windows) can't read HEIC. Bulk conversion is a one-time job you want to automate once and never think about again.

TL;DR

  • macOS: Built-in sips -s format jpeg *.heic (no install, keeps EXIF)
  • Linux/Windows: pyheif + Pillow, or ImageMagick mogrify
  • CI / serverless / no-install: ChangeThisFile API batch script below

The HEIC compatibility problem

HEIC (High Efficiency Image Container) uses H.265 compression. It's supported natively on Apple devices but not on:

  • Windows (without the paid HEIF Image Extension from the Microsoft Store)
  • Linux (without libheif)
  • Most web hosting platforms and CMSes
  • WhatsApp Web, Gmail attachment previews, Slack

JPG is universally supported. A 3MB HEIC photo becomes roughly 4-6MB as a high-quality JPG — small trade-off for zero compatibility headaches.

macOS: sips (built-in, fastest)

sips ships with every Mac and handles HEIC natively.

# Convert all HEIC in current folder to JPG (in-place, replaces extension)
for f in *.heic *.HEIC; do
  sips -s format jpeg "$f" --out "${f%.*}.jpg"
done

# Or use mogrify from ImageMagick for simpler syntax:
brew install imagemagick
mogrify -format jpg *.heic

sips preserves EXIF data (GPS, camera model, timestamps). ImageMagick also preserves EXIF by default.

Python: pyheif + Pillow

Works on Linux, Windows, and macOS without system-level ImageMagick.

pip install pyheif Pillow
# Linux also needs: apt install libheif-dev
import pyheif
from PIL import Image
from pathlib import Path
import concurrent.futures

def convert_heic(src: Path, out_dir: Path, quality: int = 90) -> str:
    heif = pyheif.read(str(src))
    img = Image.frombytes(
        heif.mode,
        heif.size,
        heif.data,
        "raw",
        heif.mode,
        heif.stride,
    )
    out_path = out_dir / src.with_suffix(".jpg").name
    img.save(out_path, "JPEG", quality=quality, optimize=True)
    return str(out_path)

def bulk_convert(input_dir: str, output_dir: str, workers: int = 4):
    src_dir = Path(input_dir)
    out_dir = Path(output_dir)
    out_dir.mkdir(parents=True, exist_ok=True)

    heic_files = list(src_dir.glob("*.heic")) + list(src_dir.glob("*.HEIC"))
    print(f"Found {len(heic_files)} HEIC files")

    with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as pool:
        futs = {pool.submit(convert_heic, f, out_dir): f for f in heic_files}
        for fut in concurrent.futures.as_completed(futs):
            src = futs[fut]
            try:
                out = fut.result()
                print(f"OK  {src.name} → {Path(out).name}")
            except Exception as e:
                print(f"ERR {src.name}: {e}")

bulk_convert("./photos", "./photos-jpg")

ChangeThisFile API batch script

Useful when you can't install pyheif (e.g., shared hosting, GitHub Actions, serverless).

# Single file
curl -X POST https://changethisfile.com/v1/convert \
  -H "Authorization: Bearer ctf_sk_your_key_here" \
  -F "file=@photo.heic" \
  -F "target=jpg" \
  --output photo.jpg
import requests
from pathlib import Path
import concurrent.futures
import time

API_KEY = "ctf_sk_your_key_here"
CONCURRENCY = 5  # stay under rate limits on free tier

def convert_one(heic_path: Path, out_dir: Path) -> str:
    with open(heic_path, "rb") as f:
        resp = requests.post(
            "https://changethisfile.com/v1/convert",
            headers={"Authorization": f"Bearer {API_KEY}"},
            files={"file": (heic_path.name, f, "image/heic")},
            data={"target": "jpg"},
            timeout=60,
        )
    resp.raise_for_status()
    out_path = out_dir / heic_path.with_suffix(".jpg").name
    out_path.write_bytes(resp.content)
    return str(out_path)

def bulk_convert_api(input_dir: str, output_dir: str):
    src_dir = Path(input_dir)
    out_dir = Path(output_dir)
    out_dir.mkdir(parents=True, exist_ok=True)
    files = list(src_dir.glob("*.heic")) + list(src_dir.glob("*.HEIC"))
    print(f"Converting {len(files)} files via API")
    with concurrent.futures.ThreadPoolExecutor(max_workers=CONCURRENCY) as pool:
        futs = {pool.submit(convert_one, f, out_dir): f for f in files}
        done = 0
        for fut in concurrent.futures.as_completed(futs):
            done += 1
            src = futs[fut]
            try:
                print(f"[{done}/{len(files)}] OK  {src.name}")
            except Exception as e:
                print(f"[{done}/{len(files)}] ERR {src.name}: {e}")

bulk_convert_api("./photos", "./photos-jpg")

Edge cases and gotchas

  • HEIC vs HEIF vs HIF. iPhones use .heic. Canon and Nikon mirrorless cameras use .hif or .heif. All three are the same container format — just rename the extension and the same tools work.
  • HEIC with multiple images. Live Photos store a still + a short video in one .heic container. pyheif extracts only the still. ImageMagick also extracts only the first frame. This is usually what you want.
  • Case sensitivity on Linux. *.heic won't match IMG_0001.HEIC on Linux (it does on macOS). Always glob both *.heic and *.HEIC.
  • EXIF orientation. Some HEIC files have EXIF rotation flags. Pillow respects them with ImageOps.exif_transpose(img) before saving. Add this call if your output images are rotated 90°.
  • File size increase. HEIC is more efficient than JPEG — a 3MB HEIC will become roughly 5-7MB JPEG at quality=90. This is normal and expected.

Scaling: 10,000+ photos

For very large batches (iPhone backup exports), parallelize conversion across CPU cores:

# GNU parallel: convert 8 files at once
ls *.heic | parallel -j8 'sips -s format jpeg {} --out {.}.jpg'

# Or with xargs on Linux:
find . -name '*.heic' | xargs -P8 -I{} bash -c 'sips -s format jpeg "$1" --out "${1%.heic}.jpg"' _ {}

On an M1 MacBook Pro, 1,000 HEIC files (avg 3MB each) convert in about 45 seconds with 8 parallel sips processes.

For the API, the free tier allows 1,000 conversions/month. Upgrade to a paid plan if you're running recurring bulk jobs.

For a one-time iPhone photo dump, macOS sips is unbeatable. For recurring pipeline work, the ChangeThisFile API removes the dependency management headache. Free tier: 1,000 conversions/month.