Batch-converting HEIC images to JPEGs on the Mac
TL:DR working around Apple proprietary brain damage
I use Lightroom 6 to manage my photo collection, although it is falling victim to bit rot (e.g. the face recognition module no longer works, apparently due to a licensing logic time bomb in the code). Exploitative pay-forever software subscriptions are simply unacceptable so I will not yield to Adobe’s Creative Clout bondage, and since Lightroom will not work in newer versions of MacOS, that means I am working on migrating to Darktable, albeit very slowly.
My wife does all her photography on her iPhone, and while the image quality is poor, she does take a great many photos and videos of our daughter. I decided to integrate them in my workflow.
To do so, I installed the free and excellent Photobackup app on her iPhone. It allows backing up her photos and videos using rsync
to my ZFS backup server, from which I rsync them to my Mac, and then use my linkonce tool to create a parallel file hierarchy that mirrors it, but so that when I delete a photo in Lightroom, it stays deleted. That way I can remove duds without having them pop back up in Lightroom every time I do a sync.
I just realized I was missing a large number of images because they are in Apple’s obnoxious HEIF format, that they switched to around the time they introduced the mostly useless Live Photos misfeature. Lightroom 6 does not recognize the format. While you can batch convert and export HEIC files to JPEG in Preview.app, it is still a manual process.
I investigated what command-line tools are available that could be run from a cron job and there are surprisingly few. GraphicsMagick sensibly refuses to support the format because of patent concerns. Most of the others require compiling an intimidating stack of dependencies first, and because HEIF is based on the H.265 HEVC video codec, an ostensibly open (in name only) ISO format that is heavily encumbered with patents, so is HEIF and it is probably illegal to use those tools like heic2jpeg.
I opted instead to write my own heic2jpeg (no relation to the previous tool). It is a very basic conversion utility using Apple’s CoreImage framework, to piggyback on Apple’s patent licenses, and as a side benefit, it will preserve the image metadata including geoloc. The flip side is that means the tool can only run on a Mac and not on Linux or Illumos, but I can live with that.
It is also my first ever Swift project. A nice expressive language in the vein of Python or Go (except with Apple’s grotesquely long API names), but I do not expect to use it much, as I have grown disillusioned with Apple’s policies and software quality, and have no intention of indenturing myself as a sharecropper in Tim Cook’s plantation any more than to Adobe’s.
The code is in heic2jpeg.swift
To build it, assuming Swift or Xcode is installed on your Mac, just run:
swiftc -O -o heic2jpeg heic2jpeg.swift
My sync script (part of my backup script) then runs something like:
find $HOME/Pictures -name \*.HEIC -print0 | xargs -0 -P 12 -t -n 10 heic2jpeg --delete
This will run 12 processes in parallel, consuming 10 files each until all HEIC are converted (or if already converted, left alone). I find the optimal setting to be 150% to 200% of the actual cores on your system (not including Intel’s fake hyperthreading cores, which do not count).
import Foundation
import CoreImage
var jpegQuality = 0.90
let context = CIContext(options: nil)
let options = NSDictionary(
dictionary: [kCGImageDestinationLossyCompressionQuality:jpegQuality]
)
var delete:Bool
var filename:String?
delete = false
for i in 1..<Int(CommandLine.argc) {
filename = CommandLine.arguments[i]
if filename == "-delete" || filename == "--delete" {
delete = true
continue
}
let srcURL = URL(fileURLWithPath:filename!)
let destURL = srcURL.deletingPathExtension().appendingPathExtension("jpg")
var exists:Bool
do {
exists = try destURL.checkResourceIsReachable()
} catch {
exists = false
}
if exists {
print("skipping \(filename ?? "???")")
} else {
print("converting \(filename ?? "???")")
let image = CIImage(contentsOf: srcURL)
try! context.writeJPEGRepresentation(
of:image!,
to:destURL,
colorSpace: image!.colorSpace!,
options:options as! [CIImageRepresentationOption : Any]
)
if delete {
print("deleting \(filename ?? "???")")
try! FileManager.default.removeItem(at:srcURL)
}
}
}