From c5b95becd25ddeeaa91f5be79d1cd1187c107648 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Sat, 29 Aug 2026 07:39:52 +0100 Subject: [PATCH] Revert "HEIC gets a thumbnail, through the decoder that can actually read it" This reverts commit f4b9596a88508522c653cad0cb3616e40fcbbc71. --- src/servers/api/file-browser/thumbnails.ts | 60 +--------------------- 1 file changed, 2 insertions(+), 58 deletions(-) diff --git a/src/servers/api/file-browser/thumbnails.ts b/src/servers/api/file-browser/thumbnails.ts index c6483a68..82de48be 100644 --- a/src/servers/api/file-browser/thumbnails.ts +++ b/src/servers/api/file-browser/thumbnails.ts @@ -1,5 +1,5 @@ import { createHash } from 'node:crypto'; -import { mkdir, rm, stat } from 'node:fs/promises'; +import { mkdir, stat } from 'node:fs/promises'; import { extname, join } from 'node:path'; import sharp from 'sharp'; @@ -179,44 +179,6 @@ const spawnFfmpeg = async (args: string[], output: string): Promise => * Errors are swallowed to `false` rather than thrown — a file sharp cannot read is a fact about the * file, and the caller already turns that into a fallback icon and a remembered failure. */ -/** Extensions sharp's bundled libheif cannot decode — see `decodeViaImageMagick`. */ -const NEEDS_IMAGEMAGICK = new Set(['heic', 'heif']); - -/** - * Get a HEIC into something sharp can actually read. - * - * ── Why this is necessary at all ── - * - * sharp ships its own libvips and libheif, built WITHOUT the HEVC decoder — so it parses a HEIC's - * header, reports 4032x3024 quite happily, and then fails on the pixels with "bad seek". That reads - * exactly like a corrupt file and is not one. Every photograph an iPhone takes is HEVC-coded HEIF, so - * this is not an edge case; it is most of what arrives from a phone. - * - * ImageMagick links the SYSTEM libheif, which does have the codec — provided - * `libheif-plugin-libde265` is installed. That package is the entire difference: without it, - * ImageMagick fails the same way with "Unsupported codec". Observed on two hosts running identical - * libheif 1.17.6, one working and one not. - * - * ── This is deliberately the same approach av-club already uses ── - * - * `plugins/av-club/sidecar/sharp/decodable.ts` solved this first and has been converting these exact - * files for weeks. Copied rather than shared because the platform must not import from a plugin — the - * dependency only runs the other way — and fifteen lines of spawn is a smaller cost than inverting that. - * - * PNG for the intermediate, not JPEG: it is lossless, so the single real encode is the final one and the - * thumbnail is not a copy of a copy. - */ -const decodeViaImageMagick = async (input: string, output: string): Promise => { - const proc = Bun.spawn(['convert', `${input}[0]`, output], { stdout: 'ignore', stderr: 'ignore' }); - const timer = setTimeout(() => proc.kill(), 30_000); - await proc.exited; - clearTimeout(timer); - // The file is the test, never the exit code — ImageMagick exits 0 having written nothing when the - // codec is missing, which is precisely the case this exists to handle. - const written = await stat(output).catch(() => null); - return written !== null && written.size > 0; -}; - const resizeImage = async (input: string, output: string, size: number): Promise => { try { await sharp(input, { failOn: 'none', limitInputPixels: false, animated: false }) @@ -275,25 +237,7 @@ export async function buildThumbnail( let ok: boolean; if (kind === 'image') { - /* - HEIC goes through ImageMagick first. Everything else — jpeg, png, webp, avif, svg — sharp reads - directly, so this costs nothing for the common case. - - The intermediate lands in the same cache directory and is removed either way: a failure here - must not leave a stray PNG that a later run mistakes for a finished thumbnail. - */ - const ext = extname(absPath).slice(1).toLowerCase(); - if (NEEDS_IMAGEMAGICK.has(ext)) { - const intermediate = `${cachePath}.decoded.png`; - try { - ok = - (await decodeViaImageMagick(absPath, intermediate)) && (await resizeImage(intermediate, cachePath, size)); - } finally { - await rm(intermediate, { force: true }); - } - } else { - ok = await resizeImage(absPath, cachePath, size); - } + ok = await resizeImage(absPath, cachePath, size); } else { // `-ss` BEFORE `-i` is the fast seek — it jumps rather than decoding a second of video first. ok = await spawnFfmpeg(['-ss', SEEK_SECONDS, '-i', absPath, ...common], cachePath);