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;
}