feat(uploads): HEIC/HEIF support + dynamic format hint on guest upload (#821)
Two of the three things from #821: - HEIC/HEIF (iPhone) can now be enabled. Sharp's bundled libvips decodes `heif` input (verified: sharp.format.heif.input.file === true on 0.34.3 / libvips 8.17.1), so thumbnails generate. Added heic/heif to EXTENSION_TO_MIME in both the backend (uploadSettings.js) and the frontend (fileTypes.ts) maps, which are kept in sync. (iOS Safari usually transcodes HEIC→JPEG at file selection, but a genuine .heic upload is now handled when it arrives.) - The upload requirements hint no longer hardcodes "JPEG, PNG or WebP". New extensionsToLabel() renders the actually-configured, supported formats (e.g. "JPG, PNG, WEBP, MOV"), and upload.fileRequirements interpolates {{formats}} across all 8 locales. Unsupported extensions are dropped from the label so it never advertises a format the backend would reject. DNG / camera RAW is deliberately NOT included: Sharp's libvips has no raw loader, so a DNG would upload then fail thumbnailing (photo → 'failed', no preview). Proper RAW support (embedded-preview extraction) is a separate PR. Adds vitest coverage for extensionsToLabel + the HEIC mapping.
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { extensionsToMimeTypes, extensionsToAcceptString, extensionsToLabel } from '../fileTypes';
|
||||
|
||||
describe('fileTypes', () => {
|
||||
describe('extensionsToMimeTypes', () => {
|
||||
it('maps known extensions to MIME types', () => {
|
||||
expect(extensionsToMimeTypes('jpg,png,mov')).toEqual(['image/jpeg', 'image/png', 'video/quicktime']);
|
||||
});
|
||||
it('supports HEIC/HEIF (#821)', () => {
|
||||
expect(extensionsToMimeTypes('heic,heif')).toEqual(['image/heic', 'image/heif']);
|
||||
});
|
||||
it('drops unknown extensions and falls back to default when nothing maps', () => {
|
||||
expect(extensionsToMimeTypes('dng,xyz')).toEqual(['image/jpeg', 'image/png', 'image/webp']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('extensionsToLabel', () => {
|
||||
it('renders a de-duplicated, upper-cased list of the configured formats', () => {
|
||||
expect(extensionsToLabel('jpg,jpeg,png,webp,mov')).toBe('JPG, JPEG, PNG, WEBP, MOV');
|
||||
});
|
||||
it('only lists supported extensions (drops unknowns like dng)', () => {
|
||||
expect(extensionsToLabel('jpg,png,dng')).toBe('JPG, PNG');
|
||||
});
|
||||
it('falls back to the default set when empty', () => {
|
||||
expect(extensionsToLabel('')).toBe('JPG, JPEG, PNG, WEBP');
|
||||
expect(extensionsToLabel(null)).toBe('JPG, JPEG, PNG, WEBP');
|
||||
});
|
||||
});
|
||||
|
||||
describe('extensionsToAcceptString', () => {
|
||||
it('joins MIME types for the input accept attribute', () => {
|
||||
expect(extensionsToAcceptString('jpg,heic')).toBe('image/jpeg,image/heic');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -12,6 +12,9 @@ const EXTENSION_TO_MIME: Record<string, string> = {
|
||||
webm: 'video/webm',
|
||||
mov: 'video/quicktime',
|
||||
avi: 'video/x-msvideo',
|
||||
// HEIC/HEIF (iPhone) — kept in sync with the backend EXTENSION_TO_MIME.
|
||||
heic: 'image/heic',
|
||||
heif: 'image/heif',
|
||||
};
|
||||
|
||||
const DEFAULT_ALLOWED = 'jpg,jpeg,png,webp';
|
||||
@@ -46,3 +49,24 @@ export function extensionsToMimeTypes(extString?: string | null): string[] {
|
||||
export function extensionsToAcceptString(extString?: string | null): string {
|
||||
return extensionsToMimeTypes(extString).join(',');
|
||||
}
|
||||
|
||||
/**
|
||||
* Human-readable, de-duplicated list of the configured extensions for the
|
||||
* upload requirements hint, e.g. "JPG, PNG, WEBP, MOV". Only extensions the
|
||||
* app actually supports (present in EXTENSION_TO_MIME) are shown, so the hint
|
||||
* never advertises a format the backend would reject.
|
||||
*/
|
||||
export function extensionsToLabel(extString?: string | null): string {
|
||||
const input = extString?.trim() || DEFAULT_ALLOWED;
|
||||
const seen = new Set<string>();
|
||||
const labels: string[] = [];
|
||||
input.split(',').forEach(ext => {
|
||||
const cleaned = ext.trim().toLowerCase().replace(/^\./, '');
|
||||
if (cleaned && EXTENSION_TO_MIME[cleaned] && !seen.has(cleaned)) {
|
||||
seen.add(cleaned);
|
||||
labels.push(cleaned.toUpperCase());
|
||||
}
|
||||
});
|
||||
if (labels.length === 0) return extensionsToLabel(DEFAULT_ALLOWED);
|
||||
return labels.join(', ');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user