image-processor-mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,99 @@
1
+ import sharp from 'sharp';
2
+ import { FileService } from './file.services.js';
3
+ import { DEFAULTS } from '../config/config.constants.js';
4
+ export class ImageService {
5
+ static async compressImage(inputPath, outputPath, options) {
6
+ const { quality = DEFAULTS.quality, lossless = DEFAULTS.lossless, effort = DEFAULTS.effort, width, height, maxDimension = DEFAULTS.maxDimension, recursiveCompress = false, expectedSizeKB = DEFAULTS.expectedSizeKB, qualityStepDown = DEFAULTS.qualityStepDown, minimumQualityFloor = DEFAULTS.minimumQualityFloor, outputFormat = DEFAULTS.outputFormat, } = options;
7
+ try {
8
+ const originalSize = await FileService.getFileSize(inputPath);
9
+ const metadata = await sharp(inputPath).metadata();
10
+ const imgWidth = metadata.width || 0;
11
+ const imgHeight = metadata.height || 0;
12
+ let resizeOptions = {};
13
+ if (width || height) {
14
+ resizeOptions = { ...(width ? { width } : {}), ...(height ? { height } : {}) };
15
+ }
16
+ else if (imgWidth > maxDimension || imgHeight > maxDimension) {
17
+ const ratio = Math.min(maxDimension / imgWidth, maxDimension / imgHeight);
18
+ resizeOptions = {
19
+ width: Math.round(imgWidth * ratio),
20
+ height: Math.round(imgHeight * ratio),
21
+ };
22
+ }
23
+ let currentQuality = quality;
24
+ let iterations = 0;
25
+ let compressedSize = 0;
26
+ const qualitySteps = [];
27
+ while (true) {
28
+ let pipeline = sharp(inputPath);
29
+ if (Object.keys(resizeOptions).length > 0) {
30
+ pipeline = pipeline.resize(resizeOptions.width, resizeOptions.height, {
31
+ fit: 'inside',
32
+ withoutEnlargement: true,
33
+ });
34
+ }
35
+ pipeline = this.applyFormat(pipeline, outputFormat, { quality: currentQuality, lossless, effort });
36
+ await pipeline.toFile(outputPath);
37
+ compressedSize = await FileService.getFileSize(outputPath);
38
+ iterations++;
39
+ qualitySteps.push(currentQuality);
40
+ const compressedKB = compressedSize / 1024;
41
+ if (recursiveCompress && compressedKB > expectedSizeKB && currentQuality - qualityStepDown >= minimumQualityFloor) {
42
+ currentQuality -= qualityStepDown;
43
+ }
44
+ else {
45
+ break;
46
+ }
47
+ }
48
+ const compressionRatio = originalSize > 0
49
+ ? parseFloat(((originalSize - compressedSize) / originalSize * 100).toFixed(2))
50
+ : 0;
51
+ return {
52
+ inputPath,
53
+ outputPath,
54
+ originalSize,
55
+ compressedSize,
56
+ compressionRatio,
57
+ success: true,
58
+ iterations,
59
+ qualitySteps,
60
+ finalQuality: qualitySteps[qualitySteps.length - 1],
61
+ exceededTarget: recursiveCompress && (compressedSize / 1024) > expectedSizeKB,
62
+ };
63
+ }
64
+ catch (error) {
65
+ return {
66
+ inputPath,
67
+ outputPath,
68
+ originalSize: 0,
69
+ compressedSize: 0,
70
+ compressionRatio: 0,
71
+ success: false,
72
+ error: error instanceof Error ? error.message : 'Unknown error',
73
+ iterations: 0,
74
+ qualitySteps: [],
75
+ finalQuality: 0,
76
+ exceededTarget: false,
77
+ };
78
+ }
79
+ }
80
+ static applyFormat(pipeline, format, options) {
81
+ const { quality, lossless, effort } = options;
82
+ switch (format) {
83
+ case 'webp':
84
+ return pipeline.webp({ quality, lossless, effort, smartSubsample: true, alphaQuality: 90 });
85
+ case 'avif':
86
+ return pipeline.avif({ quality, lossless, effort });
87
+ case 'jpeg':
88
+ return pipeline.jpeg({ quality, mozjpeg: true });
89
+ case 'png':
90
+ return pipeline.png({ compressionLevel: 9 });
91
+ case 'tiff':
92
+ return pipeline.tiff({ quality, compression: 'lzw' });
93
+ case 'gif':
94
+ return pipeline.gif();
95
+ default:
96
+ return pipeline.webp({ quality, lossless });
97
+ }
98
+ }
99
+ }
@@ -0,0 +1,4 @@
1
+ import type { CompressionResult } from '../types.js';
2
+ export declare class ReportService {
3
+ static generateReport(results: CompressionResult[], inputDir: string, outputDir: string, outputFormat: string, startTime: number, duration: number, skippedFiles?: string[]): Promise<string>;
4
+ }
@@ -0,0 +1,121 @@
1
+ import path from 'path';
2
+ import fs from 'fs-extra';
3
+ import { FileService } from './file.services.js';
4
+ import { APP_CONFIG } from '../config/app.config.js';
5
+ import { SUPPORTED_OUTPUT_FORMATS } from '../config/config.constants.js';
6
+ export class ReportService {
7
+ static async generateReport(results, inputDir, outputDir, outputFormat, startTime, duration, skippedFiles = []) {
8
+ const reportDir = path.join(outputDir, 'report');
9
+ await fs.ensureDir(reportDir);
10
+ const now = new Date();
11
+ const pad = (n) => String(n).padStart(2, '0');
12
+ const timestamp = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}-${pad(now.getHours())}-${pad(now.getMinutes())}-${pad(now.getSeconds())}`;
13
+ const reportFilename = `scan-report-${timestamp}.md`;
14
+ const reportPath = path.join(reportDir, reportFilename);
15
+ const successful = results.filter(r => r.success);
16
+ const failed = results.filter(r => !r.success);
17
+ const totalOriginalSize = results.reduce((s, r) => s + r.originalSize, 0);
18
+ const totalCompressedSize = results.reduce((s, r) => s + r.compressedSize, 0);
19
+ const overallReduction = totalOriginalSize > 0
20
+ ? ((totalOriginalSize - totalCompressedSize) / totalOriginalSize * 100).toFixed(2)
21
+ : '0.00';
22
+ const lines = [];
23
+ lines.push('# Compression Report');
24
+ lines.push('');
25
+ lines.push('**MCP Server:** ' + APP_CONFIG.name + ' v' + APP_CONFIG.version);
26
+ lines.push('**Generated:** ' + now.toLocaleString());
27
+ lines.push('');
28
+ lines.push('**Duration:** ' + (duration / 1000).toFixed(2) + 's');
29
+ lines.push('');
30
+ if (results.length > 0 && skippedFiles.length === 0) {
31
+ const extensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff', '.webp', `.${outputFormat}`];
32
+ const outputFiles = await FileService.scanFiles(outputDir, outputDir, extensions);
33
+ const tree = FileService.buildTree(outputFiles, outputDir);
34
+ const treeLines = FileService.formatTreeMarkdown(tree);
35
+ lines.push('## Summary');
36
+ lines.push('');
37
+ lines.push('| Metric | Value |');
38
+ lines.push('|--------|-------|');
39
+ lines.push(`| Total files | ${results.length} |`);
40
+ lines.push(`| Successful | ${successful.length} |`);
41
+ lines.push(`| Failed | ${failed.length} |`);
42
+ lines.push(`| Total original size | ${FileService.formatBytes(totalOriginalSize)} |`);
43
+ lines.push(`| Total compressed size | ${FileService.formatBytes(totalCompressedSize)} |`);
44
+ lines.push(`| Overall reduction | ${overallReduction}% |`);
45
+ lines.push('');
46
+ lines.push('## File Tree');
47
+ lines.push('');
48
+ lines.push('```');
49
+ lines.push(...treeLines);
50
+ lines.push('```');
51
+ lines.push('');
52
+ lines.push('## File Details');
53
+ lines.push('');
54
+ const uniqueResults = new Map();
55
+ for (const r of successful) {
56
+ uniqueResults.set(r.outputPath, r);
57
+ }
58
+ for (const r of uniqueResults.values()) {
59
+ const relPath = path.relative(outputDir, r.outputPath);
60
+ const outKB = (r.compressedSize / 1024).toFixed(2);
61
+ const inKB = (r.originalSize / 1024).toFixed(2);
62
+ const warning = r.exceededTarget ? ' ⚠ EXCEEDS LIMIT' : '';
63
+ lines.push(`### ${relPath}`);
64
+ lines.push(`- Input: ${path.basename(r.inputPath)} | ${inKB} KB`);
65
+ lines.push(`- Output: ${path.basename(r.outputPath)} | ${outKB} KB${warning}`);
66
+ lines.push(`- Compression: ${r.compressionRatio}%`);
67
+ if (r.iterations > 1) {
68
+ lines.push(`- Iterations: ${r.iterations} | Quality steps: ${r.qualitySteps.join(' → ')} → ${r.finalQuality}`);
69
+ }
70
+ lines.push('');
71
+ }
72
+ if (failed.length > 0) {
73
+ lines.push('## Failed Files');
74
+ lines.push('');
75
+ for (const r of failed) {
76
+ const relPath = path.relative(inputDir, r.inputPath);
77
+ lines.push(`- ${relPath}: ${r.error}`);
78
+ }
79
+ lines.push('');
80
+ }
81
+ if (successful.length > 0) {
82
+ lines.push('## Input vs Output Comparison');
83
+ lines.push('');
84
+ lines.push('| File | Input Size | Output Size | Compression | Status |');
85
+ lines.push('|------|-----------|------------|-------------|--------|');
86
+ for (const r of uniqueResults.values()) {
87
+ const relPath = path.relative(outputDir, r.outputPath);
88
+ const warn = (r.compressedSize / 1024) > 100 ? ' ⚠' : '';
89
+ lines.push(`| ${relPath} | ${FileService.formatBytes(r.originalSize)} | ${FileService.formatBytes(r.compressedSize)} | ${r.compressionRatio}% | ✅${warn} |`);
90
+ }
91
+ lines.push('');
92
+ }
93
+ }
94
+ if (skippedFiles.length > 0) {
95
+ const altFormats = SUPPORTED_OUTPUT_FORMATS.filter(f => f !== outputFormat);
96
+ const formatsList = altFormats.map(f => '`' + f + '`').join(', ');
97
+ lines.push('### ⚠ Notice');
98
+ lines.push('');
99
+ lines.push(`The following **${skippedFiles.length}** file(s) already have the \`.${outputFormat}\` extension and were **skipped** from compression:`);
100
+ lines.push('');
101
+ lines.push('> To compress these files:');
102
+ lines.push('> - **Option 1:** Delete the existing `.${outputFormat}` files from the input directory, then run again');
103
+ lines.push(`> - **Option 2:** Choose a different output format (e.g., ${formatsList}) instead of \`${outputFormat}\``);
104
+ lines.push('');
105
+ for (const f of skippedFiles) {
106
+ lines.push(`- \`${path.relative(path.resolve(inputDir), f)}\``);
107
+ }
108
+ lines.push('');
109
+ }
110
+ lines.push('');
111
+ lines.push('---');
112
+ lines.push('---');
113
+ lines.push('');
114
+ lines.push('**Developed by Dhvanil Pansuriya**');
115
+ lines.push('');
116
+ lines.push('[GitHub](https://github.com/pansuriyadhvanil/) · [LinkedIn](https://linkedin.com/in/dhvanil-pansuriya/)');
117
+ lines.push('');
118
+ await fs.writeFile(reportPath, lines.join('\n'), 'utf-8');
119
+ return reportPath;
120
+ }
121
+ }
@@ -0,0 +1,82 @@
1
+ export interface DownloadImageArgs {
2
+ url: string;
3
+ outputPath: string;
4
+ }
5
+ export interface CompressImageArgs {
6
+ inputPath: string;
7
+ outputPath: string;
8
+ outputFormat?: string;
9
+ quality?: number;
10
+ lossless?: boolean;
11
+ effort?: number;
12
+ width?: number;
13
+ height?: number;
14
+ maxDimension?: number;
15
+ recursiveCompress?: boolean;
16
+ expectedSizeKB?: number;
17
+ qualityStepDown?: number;
18
+ minimumQualityFloor?: number;
19
+ }
20
+ export interface CompressDirectoryArgs {
21
+ inputDir: string;
22
+ outputDir: string;
23
+ outputFormat?: string;
24
+ quality?: number;
25
+ lossless?: boolean;
26
+ effort?: number;
27
+ maxDimension?: number;
28
+ normalizeFilename?: boolean;
29
+ filenameReplaceChars?: string;
30
+ filenameCase?: string;
31
+ normalizeDirname?: boolean;
32
+ dirnameReplaceChars?: string;
33
+ dirnameCase?: string;
34
+ recursiveCompress?: boolean;
35
+ expectedSizeKB?: number;
36
+ qualityStepDown?: number;
37
+ minimumQualityFloor?: number;
38
+ }
39
+ export interface FileNamingOptions {
40
+ enabled: boolean;
41
+ replaceChars: string[];
42
+ case: 'lowercase' | 'uppercase' | 'original';
43
+ }
44
+ export interface CompressionResult {
45
+ inputPath: string;
46
+ outputPath: string;
47
+ originalSize: number;
48
+ compressedSize: number;
49
+ compressionRatio: number;
50
+ success: boolean;
51
+ error?: string;
52
+ iterations: number;
53
+ qualitySteps: number[];
54
+ finalQuality: number;
55
+ exceededTarget: boolean;
56
+ }
57
+ export interface ScanFile {
58
+ fullPath: string;
59
+ relativePath: string;
60
+ size: number;
61
+ ext: string;
62
+ nameWithoutExt: string;
63
+ }
64
+ export interface TreeNode {
65
+ name: string;
66
+ type: 'directory' | 'file';
67
+ children?: TreeNode[];
68
+ size?: number;
69
+ }
70
+ export interface CompressOptions {
71
+ quality?: number;
72
+ lossless?: boolean;
73
+ effort?: number;
74
+ width?: number;
75
+ height?: number;
76
+ maxDimension?: number;
77
+ recursiveCompress?: boolean;
78
+ expectedSizeKB?: number;
79
+ qualityStepDown?: number;
80
+ minimumQualityFloor?: number;
81
+ outputFormat?: string;
82
+ }
package/build/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "image-processor-mcp",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "MCP server for downloading, compressing, and optimizing images with batch directory processing and auto-generated reports",
6
+ "main": "build/index.js",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "lint": "tsc --noEmit",
10
+ "start": "node build/index.js"
11
+ },
12
+ "author": "Dhvanil Pansuriya",
13
+ "license": "MIT",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/pansuriyadhvanil/mcp-image-downloader.git"
17
+ },
18
+ "keywords": [
19
+ "mcp",
20
+ "image",
21
+ "downloader",
22
+ "compressor",
23
+ "optimizer",
24
+ "sharp",
25
+ "webp",
26
+ "avif",
27
+ "batch"
28
+ ],
29
+ "dependencies": {
30
+ "@modelcontextprotocol/sdk": "^1.0.4",
31
+ "axios": "^1.6.2",
32
+ "sharp": "^0.33.1",
33
+ "fs-extra": "^11.2.0"
34
+ },
35
+ "devDependencies": {
36
+ "typescript": "^5.3.3",
37
+ "@types/node": "^20.10.5",
38
+ "@types/fs-extra": "^11.0.4"
39
+ }
40
+ }
@@ -0,0 +1,4 @@
1
+ export const APP_CONFIG = {
2
+ name: 'image-processor-mcp',
3
+ version: '0.1.0',
4
+ };
@@ -0,0 +1,181 @@
1
+ export const DEFAULTS = {
2
+ quality: 85,
3
+ lossless: false,
4
+ effort: 6,
5
+ maxDimension: 2000,
6
+ expectedSizeKB: 100,
7
+ qualityStepDown: 5,
8
+ minimumQualityFloor: 10,
9
+ outputFormat: 'webp',
10
+ filenameCase: 'lowercase' as const,
11
+ dirnameCase: 'lowercase' as const,
12
+ filenameReplaceChars: [' ', '-', '.', '&'],
13
+ dirnameReplaceChars: [' ', '-', '.', '&'],
14
+ };
15
+
16
+ export const SUPPORTED_EXTENSIONS = [
17
+ '.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff', '.tif',
18
+ '.webp', '.avif', '.svg', '.heic', '.heif', '.ico', '.cur',
19
+ '.jp2', '.j2k', '.jpf', '.jpx', '.jpm', '.mj2',
20
+ '.jxr', '.wdp', '.hdp',
21
+ '.psd', '.psb',
22
+ '.dds',
23
+ '.tga', '.vda', '.icb', '.vst',
24
+ '.pbm', '.pgm', '.ppm', '.pnm', '.pfm',
25
+ '.exr',
26
+ '.hdr', '.hrd',
27
+ '.pic', '.pict', '.pct',
28
+ '.xbm', '.xpm',
29
+ '.wal',
30
+ '.cut',
31
+ '.ras', '.sun',
32
+ '.sgi', '.rgb', '.rgba', '.bw',
33
+ '.pcx',
34
+ '.pcd',
35
+ '.iff', '.lbm',
36
+ ];
37
+
38
+ export const SUPPORTED_OUTPUT_FORMATS = [
39
+ 'webp', 'avif', 'jpeg', 'png', 'tiff', 'gif',
40
+ 'heif', 'jp2', 'jxl', 'pdf',
41
+ ];
42
+
43
+ export interface ConfigDescription {
44
+ name: string;
45
+ default: string | number | boolean | string[];
46
+ description: string;
47
+ }
48
+
49
+ export const CONFIG_DESCRIPTIONS: ConfigDescription[] = [
50
+ {
51
+ name: 'quality',
52
+ default: DEFAULTS.quality,
53
+ description: 'Compression quality level for lossy formats (1-100). Higher values preserve more detail but produce larger files. Used for JPEG, WebP, AVIF, and HEIF output.',
54
+ },
55
+ {
56
+ name: 'lossless',
57
+ default: DEFAULTS.lossless,
58
+ description: 'When true, uses lossless compression. File sizes will be larger but pixel data is preserved exactly. Supported for WebP, PNG, GIF, and AVIF formats.',
59
+ },
60
+ {
61
+ name: 'effort',
62
+ default: DEFAULTS.effort,
63
+ description: 'CPU effort level (0-6). Higher values produce better compression ratios but take significantly longer to process. Level 6 gives the smallest file size.',
64
+ },
65
+ {
66
+ name: 'maxDimension',
67
+ default: DEFAULTS.maxDimension,
68
+ description: 'Maximum width or height in pixels. Images exceeding this threshold are automatically resized down to fit while maintaining aspect ratio. Set to a high value to effectively disable.',
69
+ },
70
+ {
71
+ name: 'expectedSizeKB',
72
+ default: DEFAULTS.expectedSizeKB,
73
+ description: 'Target file size in KB for recursive compression. If the compressed output exceeds this size, quality is reduced iteratively until the target is met or minimumQualityFloor is reached.',
74
+ },
75
+ {
76
+ name: 'qualityStepDown',
77
+ default: DEFAULTS.qualityStepDown,
78
+ description: 'Amount to reduce quality by in each recursive compression iteration. Larger values reach target size faster but risk visible quality degradation.',
79
+ },
80
+ {
81
+ name: 'minimumQualityFloor',
82
+ default: DEFAULTS.minimumQualityFloor,
83
+ description: 'Lowest quality value (1-100) allowed during recursive compression. Prevents excessive degradation even if the target file size is not reached.',
84
+ },
85
+ {
86
+ name: 'outputFormat',
87
+ default: DEFAULTS.outputFormat,
88
+ description: 'Default output image format. WebP offers excellent compression with good quality. AVIF gives better compression but slower encoding. JPEG offers universal compatibility.',
89
+ },
90
+ {
91
+ name: 'filenameCase',
92
+ default: DEFAULTS.filenameCase,
93
+ description: 'Case conversion applied to filenames when normalization is enabled. lowercase converts all to lower, uppercase converts all to UPPER, original leaves them as-is.',
94
+ },
95
+ {
96
+ name: 'dirnameCase',
97
+ default: DEFAULTS.dirnameCase,
98
+ description: 'Case conversion applied to directory names when normalization is enabled. Same options as filenameCase: lowercase, uppercase, or original.',
99
+ },
100
+ {
101
+ name: 'filenameReplaceChars',
102
+ default: DEFAULTS.filenameReplaceChars,
103
+ description: 'Array of characters replaced with underscores in filenames when normalization is enabled. Helps create web-safe filenames by removing spaces and special characters.',
104
+ },
105
+ {
106
+ name: 'dirnameReplaceChars',
107
+ default: DEFAULTS.dirnameReplaceChars,
108
+ description: 'Array of characters replaced with underscores in directory names when normalization is enabled. Helps create web-safe directory names.',
109
+ },
110
+ ];
111
+
112
+ export const INPUT_EXTENSION_DESCRIPTIONS: Record<string, string> = {
113
+ '.jpg': 'JPEG image (Joint Photographic Experts Group)',
114
+ '.jpeg': 'JPEG image (alternative extension)',
115
+ '.png': 'PNG image (Portable Network Graphics)',
116
+ '.gif': 'GIF image (Graphics Interchange Format)',
117
+ '.bmp': 'BMP image (Bitmap)',
118
+ '.tiff': 'TIFF image (Tagged Image File Format)',
119
+ '.tif': 'TIFF image (alternative extension)',
120
+ '.webp': 'WebP image (Google web format)',
121
+ '.avif': 'AVIF image (AV1 Image File Format)',
122
+ '.svg': 'SVG vector image (Scalable Vector Graphics)',
123
+ '.heic': 'HEIC image (High Efficiency Image Container)',
124
+ '.heif': 'HEIF image (High Efficiency Image File Format)',
125
+ '.ico': 'ICO icon file (Windows icon)',
126
+ '.cur': 'CUR cursor file (Windows cursor)',
127
+ '.jp2': 'JPEG 2000 image',
128
+ '.j2k': 'JPEG 2000 image (alternative extension)',
129
+ '.jpf': 'JPEG 2000 image (alternative extension)',
130
+ '.jpx': 'JPEG 2000 extended image',
131
+ '.jpm': 'JPEG 2000 compound image',
132
+ '.mj2': 'Motion JPEG 2000',
133
+ '.jxr': 'JPEG XR image (Microsoft HD Photo)',
134
+ '.wdp': 'Windows Media Photo (JPEG XR)',
135
+ '.hdp': 'HD Photo (JPEG XR)',
136
+ '.psd': 'Photoshop document',
137
+ '.psb': 'Photoshop large document',
138
+ '.dds': 'DirectDraw Surface texture',
139
+ '.tga': 'Targa image',
140
+ '.vda': 'Targa image (alternative extension)',
141
+ '.icb': 'Targa image (alternative extension)',
142
+ '.vst': 'Targa image (alternative extension)',
143
+ '.pbm': 'Portable Bitmap (black and white)',
144
+ '.pgm': 'Portable Graymap (grayscale)',
145
+ '.ppm': 'Portable Pixmap (color)',
146
+ '.pnm': 'Portable Anymap (generic PPM/PGM/PBM)',
147
+ '.pfm': 'Portable FloatMap (HDR)',
148
+ '.exr': 'OpenEXR HDR image (Industrial Light & Magic)',
149
+ '.hdr': 'Radiance HDR image',
150
+ '.hrd': 'Radiance HDR image (alternative extension)',
151
+ '.pic': 'PICT image (Apple QuickDraw)',
152
+ '.pict': 'PICT image (alternative extension)',
153
+ '.pct': 'PICT image (alternative extension)',
154
+ '.xbm': 'X BitMap (X11)',
155
+ '.xpm': 'X PixMap (X11)',
156
+ '.wal': 'Quake 2 texture (WAL format)',
157
+ '.cut': 'CUT image (Dr. Halo)',
158
+ '.ras': 'Sun Raster image',
159
+ '.sun': 'Sun Raster image (alternative extension)',
160
+ '.sgi': 'SGI image (Silicon Graphics)',
161
+ '.rgb': 'SGI RGB image',
162
+ '.rgba': 'SGI RGBA image',
163
+ '.bw': 'SGI black and white image',
164
+ '.pcx': 'PCX image (ZSoft PC Paintbrush)',
165
+ '.pcd': 'PhotoCD image (Kodak)',
166
+ '.iff': 'IFF image (Amiga Interchange File Format)',
167
+ '.lbm': 'IFF image (alternative extension)',
168
+ };
169
+
170
+ export const OUTPUT_FORMAT_DESCRIPTIONS: Record<string, string> = {
171
+ webp: 'Google WebP. Modern format with excellent lossy and lossless compression. Best all-around choice for web use. Supports transparency.',
172
+ avif: 'AVIF based on AV1 codec. Superior compression ratios (30-50% smaller than JPEG) but slower encoding. Best for modern browsers.',
173
+ jpeg: 'JPEG. Universal compatibility with all devices and software. Good for photographs. Does not support transparency.',
174
+ png: 'PNG. Lossless compression with full transparency support. Best for graphics, logos, screenshots, and images requiring sharp edges.',
175
+ tiff: 'TIFF. High-quality format commonly used in print and publishing. Supports layers and multiple pages. Largest file sizes.',
176
+ gif: 'GIF. Supports animation and transparency. Limited to 256 colors. Best for simple animations and low-color graphics.',
177
+ heif: 'HEIF (High Efficiency Image File Format). Modern format using HEVC codec. Better compression than JPEG. Apple ecosystem support.',
178
+ jp2: 'JPEG 2000. Wavelet-based compression with excellent quality. Used in medical imaging and digital cinema. Not widely supported in browsers.',
179
+ jxl: 'JPEG XL. Next-gen format designed to replace JPEG. Lossless re-encoding of existing JPEGs. Excellent compression. Growing browser support.',
180
+ pdf: 'PDF (Portable Document Format). Converts images to PDF pages. Useful for document creation and archiving.',
181
+ };