From 2adaaf18a14770684ca02d14c382243243714c2d Mon Sep 17 00:00:00 2001 From: root Date: Mon, 13 Apr 2026 11:08:31 +0200 Subject: [PATCH] fix: ffmpeg fallback for HEIC files that libheif rejects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit iPhone photos with depth maps or gain maps have too many auxiliary image references for libheif 1.17, causing pillow-heif to throw "Too many auxiliary image references". process_heic_image() now falls back to ffmpeg when pillow-heif fails — ffmpeg's own HEIC decoder handles these files without issue. Fixes 27/35 HEIC photos that were stuck in failed state. Co-Authored-By: Claude Opus 4.6 (1M context) --- backend/app/tasks/thumbs.py | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/backend/app/tasks/thumbs.py b/backend/app/tasks/thumbs.py index 40f45a9..85f3d6a 100644 --- a/backend/app/tasks/thumbs.py +++ b/backend/app/tasks/thumbs.py @@ -102,17 +102,36 @@ def extract_raw_preview(filepath: str) -> Optional[Image.Image]: return None def process_heic_image(filepath: str) -> Image.Image: - """Process HEIC/HEIF image formats""" + """Process HEIC/HEIF image formats. + + Tries pillow-heif first (fast, native). Falls back to ffmpeg for + files that libheif rejects — e.g. iPhone photos with too many + auxiliary image references (depth maps, gain maps). + """ try: - # Use pillow-heif to open the image img = Image.open(filepath) - # Convert to RGB if needed if img.mode != 'RGB': img = img.convert('RGB') return img except Exception as e: - logger.error(f"Error processing HEIC file {filepath}: {e}") - raise + logger.warning(f"pillow-heif failed for {filepath}: {e} — trying ffmpeg") + + # ffmpeg fallback: decode HEIC to PNG in memory. + import subprocess + from io import BytesIO + try: + result = subprocess.run( + ['ffmpeg', '-i', filepath, '-frames:v', '1', + '-f', 'image2pipe', '-vcodec', 'png', 'pipe:1'], + capture_output=True, timeout=30, stdin=subprocess.DEVNULL, + ) + if result.returncode == 0 and result.stdout: + img = Image.open(BytesIO(result.stdout)).convert('RGB') + return img + logger.error(f"ffmpeg HEIC decode failed for {filepath}: {result.stderr.decode()[-200:]}") + except Exception as e2: + logger.error(f"ffmpeg fallback failed for {filepath}: {e2}") + raise RuntimeError(f"Cannot decode HEIC: {filepath}") def process_video_thumbnail(filepath: str) -> Image.Image: """Extract a still frame from a video file as a PIL Image."""