From 558a7add89a36ff36a1fbdfd9ddbdf55af1bfb9e Mon Sep 17 00:00:00 2001 From: Kyle Bolen Date: Thu, 30 Jul 2026 03:57:44 +0000 Subject: [PATCH] HEIC thumbnails: pull full file (partial reads unreliable for sips conversion) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1MB partial HEIC reads are not sufficient for sips to convert to JPEG. Revert to full file pull for HEIC thumbnails. Files are cached permanently so each HEIC is only transferred once — subsequent launches skip already-cached files. --- .../com/bolenpad/photophetch/ui/AppState.kt | 22 +++++-------------- .../scripts/photophetch-ios-helper.py | 17 +++++++++----- 2 files changed, 17 insertions(+), 22 deletions(-) diff --git a/src/main/kotlin/com/bolenpad/photophetch/ui/AppState.kt b/src/main/kotlin/com/bolenpad/photophetch/ui/AppState.kt index 1ab1d3f..2233a4a 100644 --- a/src/main/kotlin/com/bolenpad/photophetch/ui/AppState.kt +++ b/src/main/kotlin/com/bolenpad/photophetch/ui/AppState.kt @@ -235,26 +235,16 @@ class AppState(private val configStore: ConfigStore) { val cachePath = cacheDir.resolve("$hash.jpg") if (tmpFile.toFile().exists() && tmpFile.toFile().length() > 0 && !cachePath.toFile().exists()) { val success = if (ext in setOf("heic", "heif")) { - // Extract embedded EXIF thumbnail from HEIC (fast, no full decode needed) - // sips -getProperty thumbnail reads only the thumbnail track + // Convert partial HEIC to JPEG via sips + // sips can decode HEIC even from a partial file if the header is intact val sips = ProcessBuilder( - "sips", "-getProperty", "thumbnail", + "sips", "--resampleWidth", "240", + "--setProperty", "format", "jpeg", tmpFile.toString(), "--out", cachePath.toString() ).redirectErrorStream(true).start() - val output = sips.inputStream.bufferedReader().readText() + sips.inputStream.readBytes() sips.waitFor() - val ok = cachePath.toFile().exists() && cachePath.toFile().length() > 0 - if (!ok) { - // Thumbnail extraction failed — fall back to full resample - val sips2 = ProcessBuilder( - "sips", "--resampleWidth", "240", - "--setProperty", "format", "jpeg", - tmpFile.toString(), "--out", cachePath.toString() - ).redirectErrorStream(true).start() - sips2.inputStream.readBytes() - sips2.waitFor() - cachePath.toFile().exists() && cachePath.toFile().length() > 0 - } else ok + cachePath.toFile().exists() && cachePath.toFile().length() > 0 } else { // JPEG/PNG: simple resample val sips = ProcessBuilder( diff --git a/src/main/resources/scripts/photophetch-ios-helper.py b/src/main/resources/scripts/photophetch-ios-helper.py index 06ee9ba..1a13d11 100644 --- a/src/main/resources/scripts/photophetch-ios-helper.py +++ b/src/main/resources/scripts/photophetch-ios-helper.py @@ -176,15 +176,20 @@ async def cmd_batch_thumbs(pairs_json: str): for device_path, local_path in pairs: ext = device_path.rsplit('.', 1)[-1].lower() if '.' in device_path else '' is_heic = ext in ('heic', 'heif') - read_size = 1024 * 1024 if is_heic else 512 * 1024 # 1MB for HEIC, 512KB for others try: os.makedirs(os.path.dirname(os.path.abspath(local_path)), exist_ok=True) - try: - data = await afc.fread(device_path, read_size, 0) - with open(local_path, 'wb') as f: - f.write(data) - except Exception: + if is_heic: + # HEIC: pull full file — sips needs the full image to convert to JPEG + # Cached permanently so only transferred once await afc.pull(device_path, local_path) + else: + # JPEG/PNG: 512KB partial read is sufficient for sips resample + try: + data = await afc.fread(device_path, 512 * 1024, 0) + with open(local_path, 'wb') as f: + f.write(data) + except Exception: + await afc.pull(device_path, local_path) results.append({'devicePath': device_path, 'ok': True}) except Exception as e: results.append({'devicePath': device_path, 'ok': False, 'error': str(e)})