refactor: Consolidate format mappings and FFmpeg codec configurations into shared constants and abstract FFmpeg command execution.
This commit is contained in:
@@ -10,6 +10,7 @@ import type {
|
||||
ConversionRequest,
|
||||
AudioFormat,
|
||||
} from '../core/types';
|
||||
import { MIME_TO_FORMAT } from '../core/types';
|
||||
import { getFormatCategory } from '../core/format-utils';
|
||||
import type { AudioConverterWorker } from '../workers/audio.worker';
|
||||
|
||||
@@ -85,20 +86,11 @@ export class FfmpegAdapter implements IFileConverter {
|
||||
}
|
||||
|
||||
private detectInputFormat(file: File): AudioFormat {
|
||||
const mimeToFormat: Record<string, AudioFormat> = {
|
||||
'audio/mpeg': 'mp3',
|
||||
'audio/mp3': 'mp3',
|
||||
'audio/wav': 'wav',
|
||||
'audio/wave': 'wav',
|
||||
'audio/ogg': 'ogg',
|
||||
'audio/mp4': 'm4a',
|
||||
'audio/x-m4a': 'm4a',
|
||||
'audio/aac': 'm4a',
|
||||
'audio/flac': 'flac',
|
||||
};
|
||||
|
||||
const format = mimeToFormat[file.type];
|
||||
if (format) return format;
|
||||
// Use shared MIME mappings
|
||||
const format = MIME_TO_FORMAT[file.type];
|
||||
if (format && SUPPORTED_FORMATS.includes(format as AudioFormat)) {
|
||||
return format as AudioFormat;
|
||||
}
|
||||
|
||||
// Fallback to extension
|
||||
const ext = file.name.split('.').pop()?.toLowerCase();
|
||||
|
||||
@@ -10,6 +10,7 @@ import type {
|
||||
ConversionRequest,
|
||||
ImageFormat,
|
||||
} from '../core/types';
|
||||
import { MIME_TO_FORMAT } from '../core/types';
|
||||
import { getFormatCategory } from '../core/format-utils';
|
||||
import type { ImageConverterWorker } from '../workers/image.worker';
|
||||
|
||||
@@ -82,17 +83,11 @@ export class ImageMagickAdapter implements IFileConverter {
|
||||
}
|
||||
|
||||
private detectInputFormat(file: File): ImageFormat {
|
||||
const mimeToFormat: Record<string, ImageFormat> = {
|
||||
'image/jpeg': 'jpeg',
|
||||
'image/png': 'png',
|
||||
'image/webp': 'webp',
|
||||
'image/gif': 'gif',
|
||||
'image/bmp': 'bmp',
|
||||
'image/tiff': 'tiff',
|
||||
};
|
||||
|
||||
const format = mimeToFormat[file.type];
|
||||
if (format) return format;
|
||||
// Use shared MIME mappings
|
||||
const format = MIME_TO_FORMAT[file.type];
|
||||
if (format && SUPPORTED_FORMATS.includes(format as ImageFormat)) {
|
||||
return format as ImageFormat;
|
||||
}
|
||||
|
||||
// Fallback to extension
|
||||
const ext = file.name.split('.').pop()?.toLowerCase();
|
||||
|
||||
79
src/core/constants.ts
Normal file
79
src/core/constants.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
// =============================================================================
|
||||
// Shared Constants
|
||||
// Centralized configuration for format mappings and codec settings
|
||||
// =============================================================================
|
||||
|
||||
import { MagickFormat } from '@imagemagick/magick-wasm';
|
||||
import type { AudioFormat, ImageFormat, SupportedFormat } from './types';
|
||||
import { FORMAT_METADATA } from './types';
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// MIME Type Mappings
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Convert format to MIME type (derived from FORMAT_METADATA for single source of truth)
|
||||
*/
|
||||
export const FORMAT_TO_MIME: Record<SupportedFormat, string> = Object.fromEntries(
|
||||
Object.entries(FORMAT_METADATA).map(([format, meta]) => [format, meta.mimeType])
|
||||
) as Record<SupportedFormat, string>;
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Audio Codec Configuration (FFmpeg)
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Declarative FFmpeg codec arguments per audio format.
|
||||
* Each function returns the codec arguments for the given bitrate.
|
||||
*/
|
||||
export const AUDIO_CODEC_CONFIG: Record<AudioFormat, (bitrate: number) => string[]> = {
|
||||
mp3: (br) => ['-c:a', 'libmp3lame', '-b:a', `${br}k`],
|
||||
aac: (br) => ['-c:a', 'aac', '-b:a', `${br}k`],
|
||||
m4a: (br) => ['-c:a', 'aac', '-b:a', `${br}k`],
|
||||
m4r: (br) => ['-c:a', 'aac', '-b:a', `${br}k`],
|
||||
wma: (br) => ['-c:a', 'wmav2', '-b:a', `${br}k`],
|
||||
opus: (br) => ['-c:a', 'libopus', '-b:a', `${br}k`],
|
||||
flac: () => ['-c:a', 'flac', '-compression_level', '5'],
|
||||
wav: () => ['-c:a', 'pcm_s16le'],
|
||||
aiff: () => ['-c:a', 'pcm_s16be'],
|
||||
alac: () => ['-c:a', 'alac'],
|
||||
ogg: () => ['-c:a', 'libvorbis', '-q:a', '4'],
|
||||
amr: () => ['-c:a', 'libopencore_amrnb', '-ar', '8000', '-ac', '1'],
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Image Format Mapping (ImageMagick)
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Maps our ImageFormat to ImageMagick's MagickFormat enum
|
||||
*/
|
||||
export const IMAGE_FORMAT_MAP: Record<ImageFormat, MagickFormat> = {
|
||||
jpeg: MagickFormat.Jpeg,
|
||||
png: MagickFormat.Png,
|
||||
webp: MagickFormat.WebP,
|
||||
gif: MagickFormat.Gif,
|
||||
bmp: MagickFormat.Bmp,
|
||||
tiff: MagickFormat.Tiff,
|
||||
heic: MagickFormat.Heic,
|
||||
ico: MagickFormat.Ico,
|
||||
avif: MagickFormat.Avif,
|
||||
jxl: MagickFormat.Jxl,
|
||||
svg: MagickFormat.Svg,
|
||||
psd: MagickFormat.Psd,
|
||||
raw: MagickFormat.Raw,
|
||||
tga: MagickFormat.Tga,
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats that support quality settings (lossy compression)
|
||||
*/
|
||||
export const LOSSY_IMAGE_FORMATS: ImageFormat[] = ['jpeg', 'webp', 'avif', 'jxl'];
|
||||
|
||||
/**
|
||||
* Get file extension for a format (handles special cases like ALAC)
|
||||
*/
|
||||
export function getFileExtension(format: AudioFormat): string {
|
||||
// ALAC uses m4a container
|
||||
return format === 'alac' ? 'm4a' : format;
|
||||
}
|
||||
@@ -6,11 +6,8 @@
|
||||
import * as Comlink from 'comlink';
|
||||
import { FFmpeg } from '@ffmpeg/ffmpeg';
|
||||
import { toBlobURL } from '@ffmpeg/util';
|
||||
|
||||
// All supported audio formats
|
||||
type AudioFormat =
|
||||
| 'mp3' | 'wav' | 'ogg' | 'm4a' | 'flac'
|
||||
| 'aac' | 'aiff' | 'alac' | 'wma' | 'opus' | 'm4r' | 'amr';
|
||||
import type { AudioFormat } from '../core/types';
|
||||
import { AUDIO_CODEC_CONFIG, FORMAT_TO_MIME, getFileExtension } from '../core/constants';
|
||||
|
||||
let ffmpeg: FFmpeg | null = null;
|
||||
let ffmpegLoading: Promise<void> | null = null;
|
||||
@@ -41,92 +38,61 @@ async function ensureFFmpegLoaded(): Promise<FFmpeg> {
|
||||
return ffmpeg!;
|
||||
}
|
||||
|
||||
function getFFmpegArgs(
|
||||
_inputFormat: AudioFormat,
|
||||
outputFormat: AudioFormat,
|
||||
bitrate?: number
|
||||
): string[] {
|
||||
const args: string[] = [];
|
||||
const br = bitrate || 192;
|
||||
// -----------------------------------------------------------------------------
|
||||
// Command Executor (DRY: Handles Write → Run → Read → Clean lifecycle)
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// Codec selection based on output format
|
||||
switch (outputFormat) {
|
||||
case 'mp3':
|
||||
args.push('-c:a', 'libmp3lame');
|
||||
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');
|
||||
break;
|
||||
case 'm4a':
|
||||
args.push('-c:a', 'aac');
|
||||
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;
|
||||
/**
|
||||
* Runs an FFmpeg command with guaranteed cleanup.
|
||||
* This abstracts the entire file I/O lifecycle.
|
||||
*/
|
||||
async function runFFmpegCommand(
|
||||
inputBuffer: ArrayBuffer,
|
||||
inputExt: string,
|
||||
outputExt: string,
|
||||
args: string[],
|
||||
onProgress?: (progress: number) => void
|
||||
): Promise<Uint8Array> {
|
||||
const ff = await ensureFFmpegLoaded();
|
||||
const inputFile = `input.${inputExt}`;
|
||||
const outputFile = `output.${outputExt}`;
|
||||
|
||||
// Store callback reference for proper cleanup
|
||||
const progressCallback = onProgress
|
||||
? ({ progress }: { progress: number }) => onProgress(Math.round(progress * 100))
|
||||
: null;
|
||||
|
||||
if (progressCallback) {
|
||||
ff.on('progress', progressCallback);
|
||||
}
|
||||
|
||||
return args;
|
||||
try {
|
||||
// Write input file to FFmpeg's virtual filesystem
|
||||
await ff.writeFile(inputFile, new Uint8Array(inputBuffer));
|
||||
|
||||
// Execute FFmpeg command
|
||||
await ff.exec(['-i', inputFile, ...args, '-y', outputFile]);
|
||||
|
||||
// Read output file
|
||||
const data = await ff.readFile(outputFile);
|
||||
return data as Uint8Array;
|
||||
} finally {
|
||||
// Guaranteed cleanup (even if exec crashes)
|
||||
try {
|
||||
await ff.deleteFile(inputFile);
|
||||
await ff.deleteFile(outputFile);
|
||||
if (progressCallback) {
|
||||
ff.off('progress', progressCallback);
|
||||
}
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getExtension(format: AudioFormat): string {
|
||||
// ALAC uses m4a container
|
||||
if (format === 'alac') return 'm4a';
|
||||
return format;
|
||||
}
|
||||
|
||||
function getMimeType(format: AudioFormat): string {
|
||||
const mimeMap: Record<AudioFormat, string> = {
|
||||
mp3: 'audio/mpeg',
|
||||
wav: 'audio/wav',
|
||||
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];
|
||||
}
|
||||
// -----------------------------------------------------------------------------
|
||||
// Audio Converter API
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
const audioConverter = {
|
||||
/**
|
||||
@@ -149,58 +115,29 @@ const audioConverter = {
|
||||
arrayBuffer: ArrayBuffer,
|
||||
inputFormat: AudioFormat,
|
||||
outputFormat: AudioFormat,
|
||||
bitrate?: number,
|
||||
bitrate: number = 192,
|
||||
onProgress?: (progress: number) => void
|
||||
): Promise<Blob> {
|
||||
const ff = await ensureFFmpegLoaded();
|
||||
|
||||
const inputExt = getExtension(inputFormat);
|
||||
const outputExt = getExtension(outputFormat);
|
||||
const inputFileName = `input.${inputExt}`;
|
||||
const outputFileName = `output.${outputExt}`;
|
||||
|
||||
// Register progress callback if provided
|
||||
if (onProgress) {
|
||||
ff.on('progress', ({ progress }) => {
|
||||
onProgress(Math.round(progress * 100));
|
||||
});
|
||||
const getArgs = AUDIO_CODEC_CONFIG[outputFormat];
|
||||
if (!getArgs) {
|
||||
throw new Error(`Unsupported output format: ${outputFormat}`);
|
||||
}
|
||||
|
||||
try {
|
||||
// Write input file to FFmpeg's virtual filesystem
|
||||
await ff.writeFile(inputFileName, new Uint8Array(arrayBuffer));
|
||||
const inputExt = getFileExtension(inputFormat);
|
||||
const outputExt = getFileExtension(outputFormat);
|
||||
|
||||
// Build FFmpeg command
|
||||
const codecArgs = getFFmpegArgs(inputFormat, outputFormat, bitrate);
|
||||
|
||||
await ff.exec([
|
||||
'-i', inputFileName,
|
||||
...codecArgs,
|
||||
'-y', // Overwrite output
|
||||
outputFileName,
|
||||
]);
|
||||
const resultData = await runFFmpegCommand(
|
||||
arrayBuffer,
|
||||
inputExt,
|
||||
outputExt,
|
||||
getArgs(bitrate),
|
||||
onProgress
|
||||
);
|
||||
|
||||
// Read output file
|
||||
const data = await ff.readFile(outputFileName);
|
||||
|
||||
// Cleanup
|
||||
await ff.deleteFile(inputFileName);
|
||||
await ff.deleteFile(outputFileName);
|
||||
|
||||
// Convert to regular ArrayBuffer to avoid SharedArrayBuffer issues
|
||||
const buffer = new ArrayBuffer((data as Uint8Array).length);
|
||||
new Uint8Array(buffer).set(data as Uint8Array);
|
||||
return new Blob([buffer], { type: getMimeType(outputFormat) });
|
||||
} catch (error) {
|
||||
// Attempt cleanup on error
|
||||
try {
|
||||
await ff.deleteFile(inputFileName);
|
||||
await ff.deleteFile(outputFileName);
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
// Copy to new buffer to avoid SharedArrayBuffer issues
|
||||
const buffer = new ArrayBuffer(resultData.length);
|
||||
new Uint8Array(buffer).set(resultData);
|
||||
return new Blob([buffer], { type: FORMAT_TO_MIME[outputFormat] });
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -210,52 +147,30 @@ const audioConverter = {
|
||||
arrayBuffer: ArrayBuffer,
|
||||
inputExtension: string,
|
||||
outputFormat: AudioFormat,
|
||||
bitrate?: number,
|
||||
bitrate: number = 192,
|
||||
onProgress?: (progress: number) => void
|
||||
): Promise<Blob> {
|
||||
const ff = await ensureFFmpegLoaded();
|
||||
|
||||
const inputFileName = `input.${inputExtension}`;
|
||||
const outputExt = getExtension(outputFormat);
|
||||
const outputFileName = `output.${outputExt}`;
|
||||
|
||||
if (onProgress) {
|
||||
ff.on('progress', ({ progress }) => {
|
||||
onProgress(Math.round(progress * 100));
|
||||
});
|
||||
const getArgs = AUDIO_CODEC_CONFIG[outputFormat];
|
||||
if (!getArgs) {
|
||||
throw new Error(`Unsupported output format: ${outputFormat}`);
|
||||
}
|
||||
|
||||
try {
|
||||
await ff.writeFile(inputFileName, new Uint8Array(arrayBuffer));
|
||||
// Add -vn (no video) to the codec args
|
||||
const args = ['-vn', ...getArgs(bitrate)];
|
||||
const outputExt = getFileExtension(outputFormat);
|
||||
|
||||
const codecArgs = getFFmpegArgs('m4a', outputFormat, bitrate);
|
||||
|
||||
await ff.exec([
|
||||
'-i', inputFileName,
|
||||
'-vn', // No video
|
||||
...codecArgs,
|
||||
'-y',
|
||||
outputFileName,
|
||||
]);
|
||||
const resultData = await runFFmpegCommand(
|
||||
arrayBuffer,
|
||||
inputExtension,
|
||||
outputExt,
|
||||
args,
|
||||
onProgress
|
||||
);
|
||||
|
||||
const data = await ff.readFile(outputFileName);
|
||||
|
||||
await ff.deleteFile(inputFileName);
|
||||
await ff.deleteFile(outputFileName);
|
||||
|
||||
// Convert to regular ArrayBuffer
|
||||
const buffer = new ArrayBuffer((data as Uint8Array).length);
|
||||
new Uint8Array(buffer).set(data as Uint8Array);
|
||||
return new Blob([buffer], { type: getMimeType(outputFormat) });
|
||||
} catch (error) {
|
||||
try {
|
||||
await ff.deleteFile(inputFileName);
|
||||
await ff.deleteFile(outputFileName);
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
// Copy to new buffer to avoid SharedArrayBuffer issues
|
||||
const buffer = new ArrayBuffer(resultData.length);
|
||||
new Uint8Array(buffer).set(resultData);
|
||||
return new Blob([buffer], { type: FORMAT_TO_MIME[outputFormat] });
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -9,14 +9,11 @@ import * as Comlink from 'comlink';
|
||||
import {
|
||||
initializeImageMagick,
|
||||
ImageMagick,
|
||||
MagickFormat,
|
||||
MagickGeometry,
|
||||
type IMagickImage,
|
||||
} from '@imagemagick/magick-wasm';
|
||||
|
||||
// All supported image formats
|
||||
type ImageFormat =
|
||||
| 'jpeg' | 'png' | 'webp' | 'gif' | 'bmp' | 'tiff' | 'heic' | 'ico'
|
||||
| 'avif' | 'jxl' | 'svg' | 'psd' | 'raw' | 'tga';
|
||||
import type { ImageFormat } from '../core/types';
|
||||
import { IMAGE_FORMAT_MAP, FORMAT_TO_MIME, LOSSY_IMAGE_FORMATS } from '../core/constants';
|
||||
|
||||
let magickInitialized = false;
|
||||
let initPromise: Promise<void> | null = null;
|
||||
@@ -38,45 +35,50 @@ async function ensureMagickInitialized(): Promise<void> {
|
||||
return initPromise;
|
||||
}
|
||||
|
||||
function getMagickFormat(format: ImageFormat): MagickFormat {
|
||||
const formatMap: Record<ImageFormat, MagickFormat> = {
|
||||
jpeg: MagickFormat.Jpeg,
|
||||
png: MagickFormat.Png,
|
||||
webp: MagickFormat.WebP,
|
||||
gif: MagickFormat.Gif,
|
||||
bmp: MagickFormat.Bmp,
|
||||
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];
|
||||
// -----------------------------------------------------------------------------
|
||||
// Image Processor (DRY: Handles Read → Modify → Write lifecycle)
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Generic image processor that handles I/O and accepts an operation callback.
|
||||
* The operation callback receives the image for manipulation (resize, quality, etc.)
|
||||
*/
|
||||
async function processImage(
|
||||
buffer: ArrayBuffer,
|
||||
outputFormat: ImageFormat,
|
||||
operation: (image: IMagickImage) => void
|
||||
): Promise<Blob> {
|
||||
await ensureMagickInitialized();
|
||||
const inputData = new Uint8Array(buffer);
|
||||
const magickFormat = IMAGE_FORMAT_MAP[outputFormat];
|
||||
|
||||
if (!magickFormat) {
|
||||
throw new Error(`Unsupported output format: ${outputFormat}`);
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
ImageMagick.read(inputData, (image) => {
|
||||
// Run the specific operation (resize, quality change, etc.)
|
||||
operation(image);
|
||||
|
||||
// Standard output handling
|
||||
image.write(magickFormat, (data) => {
|
||||
// Clone buffer to detach from SharedArrayBuffer
|
||||
const resultBuffer = new ArrayBuffer(data.length);
|
||||
new Uint8Array(resultBuffer).set(data);
|
||||
resolve(new Blob([resultBuffer], { type: FORMAT_TO_MIME[outputFormat] }));
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getMimeType(format: ImageFormat): string {
|
||||
const mimeMap: Record<ImageFormat, string> = {
|
||||
jpeg: 'image/jpeg',
|
||||
png: 'image/png',
|
||||
webp: 'image/webp',
|
||||
gif: 'image/gif',
|
||||
bmp: 'image/bmp',
|
||||
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];
|
||||
}
|
||||
// -----------------------------------------------------------------------------
|
||||
// Image Converter API
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
const imageConverter = {
|
||||
/**
|
||||
@@ -101,36 +103,17 @@ const imageConverter = {
|
||||
outputFormat: ImageFormat,
|
||||
quality: number = 85
|
||||
): Promise<Blob> {
|
||||
await ensureMagickInitialized();
|
||||
return processImage(arrayBuffer, outputFormat, (image) => {
|
||||
// ICO format has max size of 256x256
|
||||
if (outputFormat === 'ico' && (image.width > 256 || image.height > 256)) {
|
||||
const geometry = new MagickGeometry(256, 256);
|
||||
geometry.ignoreAspectRatio = false;
|
||||
image.resize(geometry);
|
||||
}
|
||||
|
||||
const inputData = new Uint8Array(arrayBuffer);
|
||||
const outputMagickFormat = getMagickFormat(outputFormat);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
ImageMagick.read(inputData, (image) => {
|
||||
// ICO format has max size of 256x256
|
||||
if (outputFormat === 'ico' && (image.width > 256 || image.height > 256)) {
|
||||
const geometry = new MagickGeometry(256, 256);
|
||||
geometry.ignoreAspectRatio = false;
|
||||
image.resize(geometry);
|
||||
}
|
||||
|
||||
// Set quality for lossy formats
|
||||
if (['jpeg', 'webp', 'avif', 'jxl'].includes(outputFormat)) {
|
||||
image.quality = quality;
|
||||
}
|
||||
|
||||
image.write(outputMagickFormat, (data) => {
|
||||
// Copy to regular ArrayBuffer to avoid SharedArrayBuffer issues
|
||||
const buffer = new ArrayBuffer(data.length);
|
||||
new Uint8Array(buffer).set(data);
|
||||
const blob = new Blob([buffer], { type: getMimeType(outputFormat) });
|
||||
resolve(blob);
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
// Set quality for lossy formats
|
||||
if (LOSSY_IMAGE_FORMATS.includes(outputFormat)) {
|
||||
image.quality = quality;
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -145,31 +128,15 @@ const imageConverter = {
|
||||
outputFormat: ImageFormat,
|
||||
quality: number = 85
|
||||
): Promise<Blob> {
|
||||
await ensureMagickInitialized();
|
||||
return processImage(arrayBuffer, outputFormat, (image) => {
|
||||
// Resize operation
|
||||
const geometry = new MagickGeometry(width, height);
|
||||
geometry.ignoreAspectRatio = false;
|
||||
image.resize(geometry);
|
||||
|
||||
const inputData = new Uint8Array(arrayBuffer);
|
||||
const outputMagickFormat = getMagickFormat(outputFormat);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
ImageMagick.read(inputData, (image) => {
|
||||
const geometry = new MagickGeometry(width, height);
|
||||
geometry.ignoreAspectRatio = false;
|
||||
image.resize(geometry);
|
||||
|
||||
if (['jpeg', 'webp', 'avif', 'jxl'].includes(outputFormat)) {
|
||||
image.quality = quality;
|
||||
}
|
||||
|
||||
image.write(outputMagickFormat, (data) => {
|
||||
const buffer = new ArrayBuffer(data.length);
|
||||
new Uint8Array(buffer).set(data);
|
||||
const blob = new Blob([buffer], { type: getMimeType(outputFormat) });
|
||||
resolve(blob);
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
// Set quality for lossy formats
|
||||
if (LOSSY_IMAGE_FORMATS.includes(outputFormat)) {
|
||||
image.quality = quality;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user