mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
feat: full HEIF/HEIC support, content-aware resize performance fix, UI improvements
- Add bidirectional HEIF support: decode (input) and encode (output) via system heif-convert/heif-enc - Add server-side WebP preview generation for non-browser-previewable formats (HEIC, TIFF) - Fix content-aware resize failing on HEIF input (decode before passing to caire) - Fix content-aware resize timeout on large images by downscaling to max 1200px and using JPEG intermediate - Add HEIF as target format in convert tool - Add loading spinner for HEIF preview decode in file store - Fix file picker not accepting HEIF files (explicit .heic,.heif,.hif extensions) - Extend frontend timeout for medium tools to 180s with 45s progress animation - Redesign rotate controls with preset buttons and compact flip section - Remove misleading savings percentage from convert tool
This commit is contained in:
@@ -154,11 +154,13 @@ function detectMagicBytes(buffer: Buffer): string | null {
|
||||
const brand = buffer.slice(8, 12).toString("ascii");
|
||||
if (brand !== "avif" && brand !== "avis") continue;
|
||||
}
|
||||
// For ftyp, verify HEIF/HEIC brand at bytes 8-11
|
||||
// For ftyp, verify HEIF/HEIC brand at bytes 8-11.
|
||||
// Covers HEVC still (heic/heix), HEVC sequence (hevc/hevx),
|
||||
// generic HEIF still/sequence (mif1/msf1), and multi-layer profiles.
|
||||
if (entry.format === "heif") {
|
||||
if (buffer.length < 12) continue;
|
||||
const brand = buffer.slice(8, 12).toString("ascii");
|
||||
if (brand !== "heic" && brand !== "heix" && brand !== "mif1") continue;
|
||||
if (!["heic", "heix", "mif1", "msf1", "hevc", "hevx"].includes(brand)) continue;
|
||||
}
|
||||
return entry.format;
|
||||
}
|
||||
|
||||
@@ -8,9 +8,8 @@ import { promisify } from "node:util";
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
/**
|
||||
* Find the HEIF decode command. macOS (Homebrew) provides `heif-dec`,
|
||||
* while Linux packages provide `heif-convert`. Both accept the same
|
||||
* `<input> <output>` argument syntax.
|
||||
* Find the HEIF decode command. Both heif-convert and heif-dec accept
|
||||
* `<input> <output>` positional arguments.
|
||||
*/
|
||||
let cachedDecodeCmd: string | null = null;
|
||||
|
||||
@@ -32,20 +31,32 @@ async function findDecodeCmd(): Promise<string> {
|
||||
* Decode a HEIC/HEIF buffer to PNG using the system HEIF decoder CLI.
|
||||
* This is needed because Sharp's bundled libheif does not include the
|
||||
* HEVC decoder required for true HEIC files (iPhone photos).
|
||||
*
|
||||
* Multi-image HEIF files (common from iPhones) cause heif-convert/heif-dec
|
||||
* to add numeric suffixes (-1, -2, ...) to the output filename. We try the
|
||||
* exact path first, then fall back to the -1 suffixed path.
|
||||
*/
|
||||
export async function decodeHeic(buffer: Buffer): Promise<Buffer> {
|
||||
const cmd = await findDecodeCmd();
|
||||
const id = randomUUID();
|
||||
const inputPath = join(tmpdir(), `heic-in-${id}.heic`);
|
||||
const outputPath = join(tmpdir(), `heic-out-${id}.png`);
|
||||
const suffixedPath = outputPath.replace(/\.png$/, "-1.png");
|
||||
|
||||
try {
|
||||
await writeFile(inputPath, buffer);
|
||||
await execFileAsync(cmd, [inputPath, outputPath], { timeout: 30_000 });
|
||||
return await readFile(outputPath);
|
||||
|
||||
// Single-image HEIF: exact filename. Multi-image: -1 suffix on first image.
|
||||
try {
|
||||
return await readFile(outputPath);
|
||||
} catch {
|
||||
return await readFile(suffixedPath);
|
||||
}
|
||||
} finally {
|
||||
await rm(inputPath, { force: true }).catch(() => {});
|
||||
await rm(outputPath, { force: true }).catch(() => {});
|
||||
await rm(suffixedPath, { force: true }).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user