Skip to content

Commit 13818da

Browse files
aster-voidclaude
andcommitted
fix: skip redundant client-side re-encoding for small images and add MIME type inference from extension
Small images (≤10MB) no longer get re-encoded to JPEG on the client, preserving quality and transparency. Adds extension-based MIME type fallback for browsers that don't report file.type (e.g. HEIC on desktop). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent c67d129 commit 13818da

File tree

3 files changed

+41
-7
lines changed

3 files changed

+41
-7
lines changed

src/lib/components/image-upload.svelte

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { removeByUrl, upload } from "$lib/data/private/storage.remote";
44
import {
55
type AllowedFolder,
6+
inferImageType,
67
isAcceptedImageType,
78
isAllowedFolder,
89
} from "$lib/shared/logic/image";
@@ -34,8 +35,10 @@
3435
async function handleFile(file: File, fromPaste = false) {
3536
error = null;
3637
37-
// Validate file type
38-
if (!isAcceptedImageType(file.type)) {
38+
// Resolve MIME type: use file.type, or infer from extension as fallback
39+
// (some browsers report empty type for HEIC/HEIF)
40+
const resolvedType = isAcceptedImageType(file.type) ? file.type : inferImageType(file.name);
41+
if (!resolvedType) {
3942
error = "Unsupported image format";
4043
return;
4144
}
@@ -65,8 +68,8 @@
6568
try {
6669
const arrayBuffer = await processedFile.arrayBuffer();
6770
const base64 = arrayBufferToBase64(arrayBuffer);
68-
// Use the validated original type - server will re-compress to WebP anyway
69-
const uploadType = isAcceptedImageType(processedFile.type) ? processedFile.type : file.type;
71+
// Use processed file's type if valid, otherwise fall back to resolved type
72+
const uploadType = isAcceptedImageType(processedFile.type) ? processedFile.type : resolvedType;
7073
const result = await upload({
7174
data: base64,
7275
type: uploadType,

src/lib/shared/logic/image-processing.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,14 @@ export function arrayBufferToBase64(buffer: ArrayBuffer): string {
1212
}
1313

1414
export async function compressImage(file: File): Promise<File> {
15-
// Skip if not an image that can be compressed
16-
if (!file.type.startsWith("image/") || file.type === "image/gif") {
15+
const SERVER_LIMIT = 10 * 1024 * 1024; // 10MB DoS protection limit
16+
17+
// Skip if not an image that can be compressed via canvas,
18+
// or if already small enough (server handles format conversion anyway)
19+
if (!file.type.startsWith("image/") || file.type === "image/gif" || file.size <= SERVER_LIMIT) {
1720
return file;
1821
}
1922

20-
const SERVER_LIMIT = 10 * 1024 * 1024; // 10MB DoS protection limit
2123
const QUALITY_STEPS = [0.9, 0.7, 0.5, 0.3];
2224
const DIMENSION_STEPS = [1920, 1440, 1080, 720];
2325

src/lib/shared/logic/image.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,35 @@ export function isAcceptedImageType(type: string): type is AcceptedImageType {
2424
return ACCEPTED_IMAGE_TYPES.some((t) => t === type);
2525
}
2626

27+
/**
28+
* Extension-to-MIME mapping for when browsers don't report file.type
29+
* (common with HEIC/HEIF on desktop browsers)
30+
*/
31+
const EXTENSION_TO_MIME: Record<string, AcceptedImageType> = {
32+
jpg: "image/jpeg",
33+
jpeg: "image/jpeg",
34+
png: "image/png",
35+
webp: "image/webp",
36+
avif: "image/avif",
37+
heic: "image/heic",
38+
heif: "image/heif",
39+
gif: "image/gif",
40+
tiff: "image/tiff",
41+
tif: "image/tiff",
42+
svg: "image/svg+xml",
43+
bmp: "image/bmp",
44+
};
45+
46+
/**
47+
* Infer MIME type from file extension (case-insensitive).
48+
* Returns undefined if extension is not recognized.
49+
*/
50+
export function inferImageType(filename: string): AcceptedImageType | undefined {
51+
const ext = filename.split(".").pop()?.toLowerCase();
52+
if (!ext) return undefined;
53+
return EXTENSION_TO_MIME[ext];
54+
}
55+
2756
/**
2857
* Allowed folder paths for uploads
2958
*/

0 commit comments

Comments
 (0)