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)})