diff --git a/index.html b/index.html index 9c0acd0..c0d8caa 100644 --- a/index.html +++ b/index.html @@ -2,7 +2,7 @@ - + K-Convert - Privacy-First File Converter diff --git a/public/logo.png b/public/logo.png new file mode 100644 index 0000000..c0b4e57 Binary files /dev/null and b/public/logo.png differ diff --git a/public/vite.svg b/public/vite.svg deleted file mode 100644 index e7b8dfb..0000000 --- a/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/adapters/ffmpeg-adapter.ts b/src/adapters/ffmpeg-adapter.ts index 709d207..a8fcbef 100644 --- a/src/adapters/ffmpeg-adapter.ts +++ b/src/adapters/ffmpeg-adapter.ts @@ -27,7 +27,10 @@ function getWorker(): WorkerType { return workerInstance; } -const SUPPORTED_FORMATS: AudioFormat[] = ['mp3', 'wav', 'ogg', 'm4a', 'flac']; +const SUPPORTED_FORMATS: AudioFormat[] = [ + 'mp3', 'wav', 'ogg', 'm4a', 'flac', + 'aac', 'aiff', 'alac', 'wma', 'opus', 'm4r', 'amr' +]; export class FfmpegAdapter implements IFileConverter { readonly name = 'ffmpeg-adapter'; diff --git a/src/adapters/heic-adapter.ts b/src/adapters/heic-adapter.ts index 4d37dde..098462f 100644 --- a/src/adapters/heic-adapter.ts +++ b/src/adapters/heic-adapter.ts @@ -17,8 +17,11 @@ import type { ImageConverterWorker } from '../workers/image.worker'; // Formats heic2any can directly output const HEIC2ANY_OUTPUTS: ImageFormat[] = ['jpeg', 'png']; -// All image formats we support as final output -const ALL_IMAGE_OUTPUTS: ImageFormat[] = ['jpeg', 'png', 'webp', 'gif', 'bmp', 'tiff', 'ico']; +// All image formats we support as final output (via two-step conversion) +const ALL_IMAGE_OUTPUTS: ImageFormat[] = [ + 'jpeg', 'png', 'webp', 'gif', 'bmp', 'tiff', 'ico', + 'avif', 'jxl', 'svg', 'psd', 'tga' +]; type WorkerType = Comlink.Remote; let workerInstance: WorkerType | null = null; diff --git a/src/adapters/image-magic-adapter.ts b/src/adapters/image-magic-adapter.ts index 0ab649e..5798582 100644 --- a/src/adapters/image-magic-adapter.ts +++ b/src/adapters/image-magic-adapter.ts @@ -27,8 +27,11 @@ function getWorker(): WorkerType { return workerInstance; } -// Formats that ImageMagick can handle -const SUPPORTED_FORMATS: ImageFormat[] = ['jpeg', 'png', 'webp', 'gif', 'bmp', 'tiff', 'ico']; +// Formats that ImageMagick can handle (excludes HEIC which uses heic-adapter) +const SUPPORTED_FORMATS: ImageFormat[] = [ + 'jpeg', 'png', 'webp', 'gif', 'bmp', 'tiff', 'ico', + 'avif', 'jxl', 'svg', 'psd', 'raw', 'tga' +]; export class ImageMagickAdapter implements IFileConverter { readonly name = 'imagemagick-adapter'; diff --git a/src/core/types.ts b/src/core/types.ts index 2cf2226..f34f732 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -6,8 +6,16 @@ // Format Definitions // ----------------------------------------------------------------------------- -export type ImageFormat = 'jpeg' | 'png' | 'webp' | 'gif' | 'bmp' | 'tiff' | 'heic' | 'ico'; -export type AudioFormat = 'mp3' | 'wav' | 'ogg' | 'm4a' | 'flac'; +// Image formats +export type ImageFormat = + | 'jpeg' | 'png' | 'webp' | 'gif' | 'bmp' | 'tiff' | 'heic' | 'ico' + | 'avif' | 'jxl' | 'svg' | 'psd' | 'raw' | 'tga'; + +// Audio formats +export type AudioFormat = + | 'mp3' | 'wav' | 'ogg' | 'm4a' | 'flac' + | 'aac' | 'aiff' | 'alac' | 'wma' | 'opus' | 'm4r' | 'amr'; + export type SupportedFormat = ImageFormat | AudioFormat; export type FormatCategory = 'image' | 'audio'; @@ -20,7 +28,7 @@ export interface FormatMetadata { } export const FORMAT_METADATA: Record = { - // Images + // Images - Common jpeg: { extension: 'jpg', mimeType: 'image/jpeg', displayName: 'JPEG', category: 'image' }, png: { extension: 'png', mimeType: 'image/png', displayName: 'PNG', category: 'image' }, webp: { extension: 'webp', mimeType: 'image/webp', displayName: 'WebP', category: 'image' }, @@ -29,16 +37,32 @@ export const FORMAT_METADATA: Record = { tiff: { extension: 'tiff', mimeType: 'image/tiff', displayName: 'TIFF', category: 'image' }, heic: { extension: 'heic', mimeType: 'image/heic', displayName: 'HEIC', category: 'image' }, ico: { extension: 'ico', mimeType: 'image/x-icon', displayName: 'ICO', category: 'image' }, - // Audio + // Images - New formats + avif: { extension: 'avif', mimeType: 'image/avif', displayName: 'AVIF', category: 'image' }, + jxl: { extension: 'jxl', mimeType: 'image/jxl', displayName: 'JPEG XL', category: 'image' }, + svg: { extension: 'svg', mimeType: 'image/svg+xml', displayName: 'SVG', category: 'image' }, + psd: { extension: 'psd', mimeType: 'image/vnd.adobe.photoshop', displayName: 'PSD', category: 'image' }, + raw: { extension: 'raw', mimeType: 'image/x-raw', displayName: 'RAW', category: 'image' }, + tga: { extension: 'tga', mimeType: 'image/x-tga', displayName: 'TGA', category: 'image' }, + // Audio - Common mp3: { extension: 'mp3', mimeType: 'audio/mpeg', displayName: 'MP3', category: 'audio' }, wav: { extension: 'wav', mimeType: 'audio/wav', displayName: 'WAV', category: 'audio' }, - ogg: { extension: 'ogg', mimeType: 'audio/ogg', displayName: 'OGG', category: 'audio' }, - m4a: { extension: 'm4a', mimeType: 'audio/mp4', displayName: 'M4A (AAC)', category: 'audio' }, + ogg: { extension: 'ogg', mimeType: 'audio/ogg', displayName: 'OGG Vorbis', category: 'audio' }, + m4a: { extension: 'm4a', mimeType: 'audio/mp4', displayName: 'M4A', category: 'audio' }, flac: { extension: 'flac', mimeType: 'audio/flac', displayName: 'FLAC', category: 'audio' }, + // Audio - New formats + aac: { extension: 'aac', mimeType: 'audio/aac', displayName: 'AAC', category: 'audio' }, + aiff: { extension: 'aiff', mimeType: 'audio/aiff', displayName: 'AIFF', category: 'audio' }, + alac: { extension: 'm4a', mimeType: 'audio/x-m4a', displayName: 'ALAC', category: 'audio' }, + wma: { extension: 'wma', mimeType: 'audio/x-ms-wma', displayName: 'WMA', category: 'audio' }, + opus: { extension: 'opus', mimeType: 'audio/opus', displayName: 'Opus', category: 'audio' }, + m4r: { extension: 'm4r', mimeType: 'audio/x-m4r', displayName: 'M4R (Ringtone)', category: 'audio' }, + amr: { extension: 'amr', mimeType: 'audio/amr', displayName: 'AMR', category: 'audio' }, }; // MIME type to format reverse lookup export const MIME_TO_FORMAT: Record = { + // Images 'image/jpeg': 'jpeg', 'image/png': 'png', 'image/webp': 'webp', @@ -49,15 +73,31 @@ export const MIME_TO_FORMAT: Record = { 'image/heif': 'heic', 'image/x-icon': 'ico', 'image/vnd.microsoft.icon': 'ico', + 'image/avif': 'avif', + 'image/jxl': 'jxl', + 'image/svg+xml': 'svg', + 'image/vnd.adobe.photoshop': 'psd', + 'image/x-raw': 'raw', + 'image/x-tga': 'tga', + 'image/x-targa': 'tga', + // Audio 'audio/mpeg': 'mp3', 'audio/mp3': 'mp3', 'audio/wav': 'wav', 'audio/wave': 'wav', + 'audio/x-wav': 'wav', 'audio/ogg': 'ogg', 'audio/mp4': 'm4a', 'audio/x-m4a': 'm4a', - 'audio/aac': 'm4a', 'audio/flac': 'flac', + 'audio/x-flac': 'flac', + 'audio/aac': 'aac', + 'audio/aiff': 'aiff', + 'audio/x-aiff': 'aiff', + 'audio/x-ms-wma': 'wma', + 'audio/opus': 'opus', + 'audio/x-m4r': 'm4r', + 'audio/amr': 'amr', }; // ----------------------------------------------------------------------------- diff --git a/src/index.css b/src/index.css index 8c685a9..e2c8f99 100644 --- a/src/index.css +++ b/src/index.css @@ -52,18 +52,18 @@ --card-foreground: oklch(0.145 0 0); --popover: oklch(1 0 0); --popover-foreground: oklch(0.145 0 0); - --primary: oklch(0.55 0.2 280); - --primary-foreground: oklch(0.985 0 0); + --primary: oklch(0.6927 0.1596 55.43); + --primary-foreground: oklch(0.98 0 0); --secondary: oklch(0.97 0 0); --secondary-foreground: oklch(0.205 0 0); --muted: oklch(0.97 0 0); --muted-foreground: oklch(0.556 0 0); - --accent: oklch(0.95 0.05 280); + --accent: oklch(0.95 0.05 55); --accent-foreground: oklch(0.205 0 0); --destructive: oklch(0.577 0.245 27.325); --border: oklch(0.922 0 0); --input: oklch(0.922 0 0); - --ring: oklch(0.55 0.2 280); + --ring: oklch(0.6927 0.1596 55.43); --chart-1: oklch(0.646 0.222 41.116); --chart-2: oklch(0.6 0.118 184.704); --chart-3: oklch(0.398 0.07 227.392); @@ -71,46 +71,46 @@ --chart-5: oklch(0.769 0.188 70.08); --sidebar: oklch(0.985 0 0); --sidebar-foreground: oklch(0.145 0 0); - --sidebar-primary: oklch(0.55 0.2 280); + --sidebar-primary: oklch(0.6927 0.1596 55.43); --sidebar-primary-foreground: oklch(0.985 0 0); --sidebar-accent: oklch(0.97 0 0); --sidebar-accent-foreground: oklch(0.205 0 0); --sidebar-border: oklch(0.922 0 0); - --sidebar-ring: oklch(0.55 0.2 280); + --sidebar-ring: oklch(0.6927 0.1596 55.43); } .dark { - --background: oklch(0.12 0.02 280); + --background: oklch(0.12 0.02 55); --foreground: oklch(0.95 0 0); - --card: oklch(0.16 0.02 280); + --card: oklch(0.16 0.02 55); --card-foreground: oklch(0.95 0 0); - --popover: oklch(0.16 0.02 280); + --popover: oklch(0.16 0.02 55); --popover-foreground: oklch(0.95 0 0); - --primary: oklch(0.7 0.22 280); - --primary-foreground: oklch(0.12 0.02 280); - --secondary: oklch(0.22 0.03 280); + --primary: oklch(0.7 0.2 55); + --primary-foreground: oklch(0.12 0.02 55); + --secondary: oklch(0.22 0.03 55); --secondary-foreground: oklch(0.95 0 0); - --muted: oklch(0.22 0.03 280); + --muted: oklch(0.22 0.03 55); --muted-foreground: oklch(0.65 0 0); - --accent: oklch(0.28 0.06 280); + --accent: oklch(0.28 0.06 55); --accent-foreground: oklch(0.95 0 0); --destructive: oklch(0.65 0.22 25); --border: oklch(1 0 0 / 12%); --input: oklch(1 0 0 / 15%); - --ring: oklch(0.7 0.22 280); - --chart-1: oklch(0.7 0.22 280); + --ring: oklch(0.7 0.2 55); + --chart-1: oklch(0.7 0.2 55); --chart-2: oklch(0.696 0.17 162.48); --chart-3: oklch(0.769 0.188 70.08); --chart-4: oklch(0.627 0.265 303.9); --chart-5: oklch(0.645 0.246 16.439); - --sidebar: oklch(0.14 0.02 280); + --sidebar: oklch(0.14 0.02 55); --sidebar-foreground: oklch(0.95 0 0); - --sidebar-primary: oklch(0.7 0.22 280); - --sidebar-primary-foreground: oklch(0.12 0.02 280); - --sidebar-accent: oklch(0.22 0.03 280); + --sidebar-primary: oklch(0.7 0.2 55); + --sidebar-primary-foreground: oklch(0.12 0.02 55); + --sidebar-accent: oklch(0.22 0.03 55); --sidebar-accent-foreground: oklch(0.95 0 0); --sidebar-border: oklch(1 0 0 / 12%); - --sidebar-ring: oklch(0.7 0.22 280); + --sidebar-ring: oklch(0.7 0.2 55); } @layer base { diff --git a/src/workers/audio.worker.ts b/src/workers/audio.worker.ts index 694e84f..cc39454 100644 --- a/src/workers/audio.worker.ts +++ b/src/workers/audio.worker.ts @@ -7,7 +7,10 @@ import * as Comlink from 'comlink'; import { FFmpeg } from '@ffmpeg/ffmpeg'; import { toBlobURL } from '@ffmpeg/util'; -type AudioFormat = 'mp3' | 'wav' | 'ogg' | 'm4a' | 'flac'; +// All supported audio formats +type AudioFormat = + | 'mp3' | 'wav' | 'ogg' | 'm4a' | 'flac' + | 'aac' | 'aiff' | 'alac' | 'wma' | 'opus' | 'm4r' | 'amr'; let ffmpeg: FFmpeg | null = null; let ffmpegLoading: Promise | null = null; @@ -30,6 +33,8 @@ async function ensureFFmpegLoaded(): Promise { coreURL: await toBlobURL(`${baseURL}/ffmpeg-core.js`, 'text/javascript'), wasmURL: await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, 'application/wasm'), }); + + console.log('[AudioWorker] FFmpeg WASM initialized'); })(); await ffmpegLoading; @@ -42,34 +47,66 @@ function getFFmpegArgs( bitrate?: number ): string[] { const args: string[] = []; + const br = bitrate || 192; // Codec selection based on output format switch (outputFormat) { case 'mp3': args.push('-c:a', 'libmp3lame'); - args.push('-b:a', `${bitrate || 192}k`); + args.push('-b:a', `${br}k`); break; case 'wav': args.push('-c:a', 'pcm_s16le'); break; case 'ogg': args.push('-c:a', 'libvorbis'); - args.push('-q:a', '4'); // Quality level 0-10 + args.push('-q:a', '4'); break; case 'm4a': args.push('-c:a', 'aac'); - args.push('-b:a', `${bitrate || 192}k`); + args.push('-b:a', `${br}k`); break; case 'flac': args.push('-c:a', 'flac'); args.push('-compression_level', '5'); break; + case 'aac': + args.push('-c:a', 'aac'); + args.push('-b:a', `${br}k`); + break; + case 'aiff': + args.push('-c:a', 'pcm_s16be'); + break; + case 'alac': + // ALAC in M4A container + args.push('-c:a', 'alac'); + break; + case 'wma': + args.push('-c:a', 'wmav2'); + args.push('-b:a', `${br}k`); + break; + case 'opus': + args.push('-c:a', 'libopus'); + args.push('-b:a', `${br}k`); + break; + case 'm4r': + // M4R is just AAC with different extension (iPhone ringtones) + args.push('-c:a', 'aac'); + args.push('-b:a', `${br}k`); + break; + case 'amr': + args.push('-c:a', 'libopencore_amrnb'); + args.push('-ar', '8000'); // AMR requires 8kHz sample rate + args.push('-ac', '1'); // AMR is mono only + break; } return args; } function getExtension(format: AudioFormat): string { + // ALAC uses m4a container + if (format === 'alac') return 'm4a'; return format; } @@ -80,11 +117,31 @@ function getMimeType(format: AudioFormat): string { ogg: 'audio/ogg', m4a: 'audio/mp4', flac: 'audio/flac', + aac: 'audio/aac', + aiff: 'audio/aiff', + alac: 'audio/x-m4a', + wma: 'audio/x-ms-wma', + opus: 'audio/opus', + m4r: 'audio/x-m4r', + amr: 'audio/amr', }; return mimeMap[format]; } const audioConverter = { + /** + * Initialize the worker (can be called early to warm up) + */ + async init(): Promise { + try { + await ensureFFmpegLoaded(); + return true; + } catch (error) { + console.error('[AudioWorker] Init failed:', error); + return false; + } + }, + /** * Convert audio file using FFmpeg */ diff --git a/src/workers/image.worker.ts b/src/workers/image.worker.ts index ebd8e83..ef472df 100644 --- a/src/workers/image.worker.ts +++ b/src/workers/image.worker.ts @@ -13,7 +13,10 @@ import { MagickGeometry, } from '@imagemagick/magick-wasm'; -type ImageFormat = 'jpeg' | 'png' | 'webp' | 'gif' | 'bmp' | 'tiff' | 'heic' | 'ico'; +// All supported image formats +type ImageFormat = + | 'jpeg' | 'png' | 'webp' | 'gif' | 'bmp' | 'tiff' | 'heic' | 'ico' + | 'avif' | 'jxl' | 'svg' | 'psd' | 'raw' | 'tga'; let magickInitialized = false; let initPromise: Promise | null = null; @@ -46,6 +49,12 @@ function getMagickFormat(format: ImageFormat): MagickFormat { tiff: MagickFormat.Tiff, heic: MagickFormat.Heic, ico: MagickFormat.Ico, + avif: MagickFormat.Avif, + jxl: MagickFormat.Jxl, + svg: MagickFormat.Svg, + psd: MagickFormat.Psd, + raw: MagickFormat.Raw, // Note: May need specific raw format handling + tga: MagickFormat.Tga, }; return formatMap[format]; } @@ -60,6 +69,12 @@ function getMimeType(format: ImageFormat): string { tiff: 'image/tiff', heic: 'image/heic', ico: 'image/x-icon', + avif: 'image/avif', + jxl: 'image/jxl', + svg: 'image/svg+xml', + psd: 'image/vnd.adobe.photoshop', + raw: 'image/x-raw', + tga: 'image/x-tga', }; return mimeMap[format]; } @@ -103,7 +118,7 @@ const imageConverter = { } // Set quality for lossy formats - if (outputFormat === 'jpeg' || outputFormat === 'webp') { + if (['jpeg', 'webp', 'avif', 'jxl'].includes(outputFormat)) { image.quality = quality; } @@ -143,7 +158,7 @@ const imageConverter = { geometry.ignoreAspectRatio = false; image.resize(geometry); - if (outputFormat === 'jpeg' || outputFormat === 'webp') { + if (['jpeg', 'webp', 'avif', 'jxl'].includes(outputFormat)) { image.quality = quality; }