mirror of
https://github.com/iib0011/omni-tools.git
synced 2025-11-07 09:24:55 +05:30
feat: add HEIC Converter tool for converting HEIC/HEIF images to JPG, PNG, or WebP formats with options for quality and resizing
This commit is contained in:
parent
fc18dc0dc0
commit
0a33958490
8 changed files with 559 additions and 2 deletions
|
|
@ -101,5 +101,56 @@
|
||||||
"description": "Rotate an image by a specified angle.",
|
"description": "Rotate an image by a specified angle.",
|
||||||
"shortDescription": "Rotate an image easily.",
|
"shortDescription": "Rotate an image easily.",
|
||||||
"title": "Rotate Image"
|
"title": "Rotate Image"
|
||||||
|
},
|
||||||
|
"heicConverter": {
|
||||||
|
"title": "HEIC Converter",
|
||||||
|
"description": "Convert HEIC/HEIF images to JPG, PNG, or WebP formats with customizable quality and resize options.",
|
||||||
|
"shortDescription": "Convert HEIC/HEIF images to common formats",
|
||||||
|
"longDescription": "Convert HEIC and HEIF images (commonly used by Apple devices) to widely supported formats like JPG, PNG, or WebP. Features include quality control, resizing options, and file size optimization.",
|
||||||
|
"input": {
|
||||||
|
"title": "Upload HEIC/HEIF Image"
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"outputFormat": {
|
||||||
|
"title": "Output Format",
|
||||||
|
"jpg": "JPEG (JPG)",
|
||||||
|
"png": "PNG",
|
||||||
|
"webp": "WebP"
|
||||||
|
},
|
||||||
|
"quality": {
|
||||||
|
"description": "Quality setting for JPEG output (1-100, higher = better quality)"
|
||||||
|
},
|
||||||
|
"resize": {
|
||||||
|
"title": "Resize Options",
|
||||||
|
"none": "No resize",
|
||||||
|
"width": "Set width",
|
||||||
|
"height": "Set height",
|
||||||
|
"both": "Set both width and height"
|
||||||
|
},
|
||||||
|
"resizeValue": {
|
||||||
|
"description": "Size in pixels for the selected resize mode"
|
||||||
|
},
|
||||||
|
"advanced": {
|
||||||
|
"title": "Advanced Options"
|
||||||
|
},
|
||||||
|
"preserveMetadata": {
|
||||||
|
"title": "Preserve Metadata",
|
||||||
|
"description": "Keep EXIF data and other image metadata (when possible)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"result": {
|
||||||
|
"title": "Converted Image",
|
||||||
|
"originalSize": "Original Size",
|
||||||
|
"convertedSize": "Converted Size",
|
||||||
|
"compression": "Compression"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"invalidFile": "Please upload a valid HEIC or HEIF image file.",
|
||||||
|
"conversionFailed": "Failed to convert the image. Please try again."
|
||||||
|
},
|
||||||
|
"info": {
|
||||||
|
"title": "What is HEIC Converter?",
|
||||||
|
"description": "HEIC (High Efficiency Image Container) is Apple's image format that provides better compression than JPEG while maintaining high quality. This tool converts HEIC/HEIF files to widely supported formats for better compatibility across devices and platforms."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ export async function changeAudioSpeed(
|
||||||
const outputName = `output.${outputFormat}`;
|
const outputName = `output.${outputFormat}`;
|
||||||
await ffmpeg.writeFile(fileName, await fetchFile(input));
|
await ffmpeg.writeFile(fileName, await fetchFile(input));
|
||||||
const audioFilter = computeAudioFilter(newSpeed);
|
const audioFilter = computeAudioFilter(newSpeed);
|
||||||
let args = ['-i', fileName, '-filter:a', audioFilter];
|
const args = ['-i', fileName, '-filter:a', audioFilter];
|
||||||
if (outputFormat === 'mp3') {
|
if (outputFormat === 'mp3') {
|
||||||
args.push('-b:a', '192k', '-f', 'mp3', outputName);
|
args.push('-b:a', '192k', '-f', 'mp3', outputName);
|
||||||
} else if (outputFormat === 'aac') {
|
} else if (outputFormat === 'aac') {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
import { expect, describe, it } from 'vitest';
|
||||||
|
import { isHeicFile, formatFileSize } from './service';
|
||||||
|
|
||||||
|
// Mock File constructor
|
||||||
|
const createMockFile = (name: string, type: string): File => {
|
||||||
|
return new File(['mock content'], name, { type });
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('HEIC Converter Service', () => {
|
||||||
|
describe('isHeicFile', () => {
|
||||||
|
it('should return true for HEIC files', () => {
|
||||||
|
const heicFile = createMockFile('test.heic', 'image/heic');
|
||||||
|
expect(isHeicFile(heicFile)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true for HEIF files', () => {
|
||||||
|
const heifFile = createMockFile('test.heif', 'image/heif');
|
||||||
|
expect(isHeicFile(heifFile)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true for files with .heic extension', () => {
|
||||||
|
const heicFile = createMockFile('test.heic', 'application/octet-stream');
|
||||||
|
expect(isHeicFile(heicFile)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true for files with .heif extension', () => {
|
||||||
|
const heifFile = createMockFile('test.heif', 'application/octet-stream');
|
||||||
|
expect(isHeicFile(heifFile)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for non-HEIC files', () => {
|
||||||
|
const jpgFile = createMockFile('test.jpg', 'image/jpeg');
|
||||||
|
const pngFile = createMockFile('test.png', 'image/png');
|
||||||
|
|
||||||
|
expect(isHeicFile(jpgFile)).toBe(false);
|
||||||
|
expect(isHeicFile(pngFile)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('formatFileSize', () => {
|
||||||
|
it('should format bytes correctly', () => {
|
||||||
|
expect(formatFileSize(0)).toBe('0 Bytes');
|
||||||
|
expect(formatFileSize(1024)).toBe('1 KB');
|
||||||
|
expect(formatFileSize(1048576)).toBe('1 MB');
|
||||||
|
expect(formatFileSize(1073741824)).toBe('1 GB');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle decimal values', () => {
|
||||||
|
expect(formatFileSize(1536)).toBe('1.5 KB');
|
||||||
|
expect(formatFileSize(1572864)).toBe('1.5 MB');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle large file sizes', () => {
|
||||||
|
expect(formatFileSize(2147483648)).toBe('2 GB');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
228
src/pages/tools/image/generic/heic-converter/index.tsx
Normal file
228
src/pages/tools/image/generic/heic-converter/index.tsx
Normal file
|
|
@ -0,0 +1,228 @@
|
||||||
|
import { Box, Alert, Chip } from '@mui/material';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import ToolContent from '@components/ToolContent';
|
||||||
|
import { ToolComponentProps } from '@tools/defineTool';
|
||||||
|
import ToolImageInput from '@components/input/ToolImageInput';
|
||||||
|
import ToolFileResult from '@components/result/ToolFileResult';
|
||||||
|
import { GetGroupsType } from '@components/options/ToolOptions';
|
||||||
|
import { convertHeicImage, isHeicFile, formatFileSize } from './service';
|
||||||
|
import { InitialValuesType, ConversionResult } from './types';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import RadioWithTextField from '@components/options/RadioWithTextField';
|
||||||
|
import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
|
||||||
|
import CheckboxWithDesc from '@components/options/CheckboxWithDesc';
|
||||||
|
|
||||||
|
const initialValues: InitialValuesType = {
|
||||||
|
outputFormat: 'jpg',
|
||||||
|
quality: 85,
|
||||||
|
preserveMetadata: false,
|
||||||
|
resizeMode: 'none',
|
||||||
|
resizeValue: 1920
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function HeicConverter({
|
||||||
|
title,
|
||||||
|
longDescription
|
||||||
|
}: ToolComponentProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [input, setInput] = useState<File | null>(null);
|
||||||
|
const [result, setResult] = useState<ConversionResult | null>(null);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const compute = async (values: InitialValuesType, input: File | null) => {
|
||||||
|
if (!input) return;
|
||||||
|
|
||||||
|
if (!isHeicFile(input)) {
|
||||||
|
setError(t('image:heicConverter.error.invalidFile'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setError(null);
|
||||||
|
setResult(null);
|
||||||
|
|
||||||
|
const conversionResult = await convertHeicImage(input, values);
|
||||||
|
setResult(conversionResult);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('HEIC conversion failed:', err);
|
||||||
|
setError(t('image:heicConverter.error.conversionFailed'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getGroups: GetGroupsType<InitialValuesType> | null = ({
|
||||||
|
values,
|
||||||
|
updateField
|
||||||
|
}) => [
|
||||||
|
{
|
||||||
|
title: t('image:heicConverter.options.outputFormat.title'),
|
||||||
|
component: (
|
||||||
|
<Box>
|
||||||
|
<RadioWithTextField
|
||||||
|
value={values.outputFormat}
|
||||||
|
onTextChange={(value: any) => updateField('outputFormat', value)}
|
||||||
|
options={[
|
||||||
|
{
|
||||||
|
value: 'jpg',
|
||||||
|
label: t('image:heicConverter.options.outputFormat.jpg')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'png',
|
||||||
|
label: t('image:heicConverter.options.outputFormat.png')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'webp',
|
||||||
|
label: t('image:heicConverter.options.outputFormat.webp')
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{values.outputFormat === 'jpg' && (
|
||||||
|
<TextFieldWithDesc
|
||||||
|
value={values.quality.toString()}
|
||||||
|
onOwnChange={(value) =>
|
||||||
|
updateField('quality', parseInt(value) || 85)
|
||||||
|
}
|
||||||
|
description={t('image:heicConverter.options.quality.description')}
|
||||||
|
inputProps={{
|
||||||
|
type: 'number',
|
||||||
|
min: 1,
|
||||||
|
max: 100,
|
||||||
|
'data-testid': 'quality-input'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('image:heicConverter.options.resize.title'),
|
||||||
|
component: (
|
||||||
|
<Box>
|
||||||
|
<RadioWithTextField
|
||||||
|
value={values.resizeMode}
|
||||||
|
onTextChange={(value: any) => updateField('resizeMode', value)}
|
||||||
|
options={[
|
||||||
|
{
|
||||||
|
value: 'none',
|
||||||
|
label: t('image:heicConverter.options.resize.none')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'width',
|
||||||
|
label: t('image:heicConverter.options.resize.width')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'height',
|
||||||
|
label: t('image:heicConverter.options.resize.height')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'both',
|
||||||
|
label: t('image:heicConverter.options.resize.both')
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{values.resizeMode !== 'none' && (
|
||||||
|
<TextFieldWithDesc
|
||||||
|
value={values.resizeValue.toString()}
|
||||||
|
onOwnChange={(value) =>
|
||||||
|
updateField('resizeValue', parseInt(value) || 1920)
|
||||||
|
}
|
||||||
|
description={t(
|
||||||
|
'image:heicConverter.options.resizeValue.description'
|
||||||
|
)}
|
||||||
|
inputProps={{
|
||||||
|
type: 'number',
|
||||||
|
min: 1,
|
||||||
|
'data-testid': 'resize-value-input'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('image:heicConverter.options.advanced.title'),
|
||||||
|
component: (
|
||||||
|
<Box>
|
||||||
|
<CheckboxWithDesc
|
||||||
|
title={t('image:heicConverter.options.preserveMetadata.title')}
|
||||||
|
checked={values.preserveMetadata}
|
||||||
|
onChange={(value) => updateField('preserveMetadata', value)}
|
||||||
|
description={t(
|
||||||
|
'image:heicConverter.options.preserveMetadata.description'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToolContent
|
||||||
|
title={title}
|
||||||
|
input={input}
|
||||||
|
setInput={setInput}
|
||||||
|
initialValues={initialValues}
|
||||||
|
compute={compute}
|
||||||
|
inputComponent={
|
||||||
|
<ToolImageInput
|
||||||
|
value={input}
|
||||||
|
onChange={setInput}
|
||||||
|
accept={['image/heic', 'image/heif']}
|
||||||
|
title={t('image:heicConverter.input.title')}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
resultComponent={
|
||||||
|
<Box>
|
||||||
|
{error && (
|
||||||
|
<Alert severity="error" sx={{ mb: 2 }}>
|
||||||
|
{error}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{result && (
|
||||||
|
<Box>
|
||||||
|
<ToolFileResult
|
||||||
|
title={t('image:heicConverter.result.title')}
|
||||||
|
value={result.file}
|
||||||
|
extension={result.format}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Box sx={{ mt: 2, display: 'flex', gap: 2, flexWrap: 'wrap' }}>
|
||||||
|
<Chip
|
||||||
|
label={`${t(
|
||||||
|
'image:heicConverter.result.originalSize'
|
||||||
|
)}: ${formatFileSize(result.originalSize)}`}
|
||||||
|
variant="outlined"
|
||||||
|
color="primary"
|
||||||
|
/>
|
||||||
|
<Chip
|
||||||
|
label={`${t(
|
||||||
|
'image:heicConverter.result.convertedSize'
|
||||||
|
)}: ${formatFileSize(result.convertedSize)}`}
|
||||||
|
variant="outlined"
|
||||||
|
color="secondary"
|
||||||
|
/>
|
||||||
|
<Chip
|
||||||
|
label={`${t(
|
||||||
|
'image:heicConverter.result.compression'
|
||||||
|
)}: ${Math.round(
|
||||||
|
(1 - result.convertedSize / result.originalSize) * 100
|
||||||
|
)}%`}
|
||||||
|
variant="outlined"
|
||||||
|
color="success"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
getGroups={getGroups}
|
||||||
|
toolInfo={{
|
||||||
|
title: t('image:heicConverter.info.title'),
|
||||||
|
description:
|
||||||
|
longDescription || t('image:heicConverter.info.description')
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
24
src/pages/tools/image/generic/heic-converter/meta.ts
Normal file
24
src/pages/tools/image/generic/heic-converter/meta.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { defineTool } from '@tools/defineTool';
|
||||||
|
import { lazy } from 'react';
|
||||||
|
|
||||||
|
export const tool = defineTool('image-generic', {
|
||||||
|
i18n: {
|
||||||
|
name: 'image:heicConverter.title',
|
||||||
|
description: 'image:heicConverter.description',
|
||||||
|
shortDescription: 'image:heicConverter.shortDescription',
|
||||||
|
longDescription: 'image:heicConverter.longDescription'
|
||||||
|
},
|
||||||
|
path: 'heic-converter',
|
||||||
|
icon: 'mdi:image-multiple-outline',
|
||||||
|
keywords: [
|
||||||
|
'heic',
|
||||||
|
'heif',
|
||||||
|
'converter',
|
||||||
|
'image',
|
||||||
|
'format',
|
||||||
|
'convert',
|
||||||
|
'iphone',
|
||||||
|
'apple'
|
||||||
|
],
|
||||||
|
component: lazy(() => import('./index'))
|
||||||
|
});
|
||||||
180
src/pages/tools/image/generic/heic-converter/service.ts
Normal file
180
src/pages/tools/image/generic/heic-converter/service.ts
Normal file
|
|
@ -0,0 +1,180 @@
|
||||||
|
import { InitialValuesType, ConversionResult } from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts HEIC/HEIF images to other formats
|
||||||
|
* Uses browser's native capabilities and canvas for conversion
|
||||||
|
*/
|
||||||
|
export async function convertHeicImage(
|
||||||
|
file: File,
|
||||||
|
options: InitialValuesType
|
||||||
|
): Promise<ConversionResult> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
|
if (!ctx) {
|
||||||
|
reject(new Error('Canvas context not available'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const img = new Image();
|
||||||
|
|
||||||
|
img.onload = () => {
|
||||||
|
try {
|
||||||
|
// Calculate dimensions based on resize mode
|
||||||
|
let { width, height } = calculateDimensions(
|
||||||
|
img.width,
|
||||||
|
img.height,
|
||||||
|
options.resizeMode,
|
||||||
|
options.resizeValue
|
||||||
|
);
|
||||||
|
|
||||||
|
// Set canvas dimensions
|
||||||
|
canvas.width = width;
|
||||||
|
canvas.height = height;
|
||||||
|
|
||||||
|
// Draw image with white background (for transparency handling)
|
||||||
|
ctx.fillStyle = '#ffffff';
|
||||||
|
ctx.fillRect(0, 0, width, height);
|
||||||
|
ctx.drawImage(img, 0, 0, width, height);
|
||||||
|
|
||||||
|
// Convert to desired format
|
||||||
|
const mimeType = getMimeType(options.outputFormat);
|
||||||
|
const quality =
|
||||||
|
options.outputFormat === 'jpg' ? options.quality / 100 : 1;
|
||||||
|
|
||||||
|
canvas.toBlob(
|
||||||
|
(blob) => {
|
||||||
|
if (!blob) {
|
||||||
|
reject(new Error('Failed to convert image'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileName = generateFileName(file.name, options.outputFormat);
|
||||||
|
const convertedFile = new File([blob], fileName, {
|
||||||
|
type: mimeType
|
||||||
|
});
|
||||||
|
|
||||||
|
const result: ConversionResult = {
|
||||||
|
file: convertedFile,
|
||||||
|
originalSize: file.size,
|
||||||
|
convertedSize: convertedFile.size,
|
||||||
|
format: options.outputFormat
|
||||||
|
};
|
||||||
|
|
||||||
|
resolve(result);
|
||||||
|
},
|
||||||
|
mimeType,
|
||||||
|
quality
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
img.onerror = () => {
|
||||||
|
reject(new Error('Failed to load HEIC image'));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create object URL for the image
|
||||||
|
const objectUrl = URL.createObjectURL(file);
|
||||||
|
img.src = objectUrl;
|
||||||
|
|
||||||
|
// Clean up object URL after loading
|
||||||
|
img.onload = () => {
|
||||||
|
URL.revokeObjectURL(objectUrl);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate new dimensions based on resize mode
|
||||||
|
*/
|
||||||
|
function calculateDimensions(
|
||||||
|
originalWidth: number,
|
||||||
|
originalHeight: number,
|
||||||
|
resizeMode: InitialValuesType['resizeMode'],
|
||||||
|
resizeValue: number
|
||||||
|
): { width: number; height: number } {
|
||||||
|
switch (resizeMode) {
|
||||||
|
case 'width': {
|
||||||
|
const aspectRatio = originalWidth / originalHeight;
|
||||||
|
return {
|
||||||
|
width: resizeValue,
|
||||||
|
height: Math.round(resizeValue / aspectRatio)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case 'height': {
|
||||||
|
const aspectRatio2 = originalWidth / originalHeight;
|
||||||
|
return {
|
||||||
|
width: Math.round(resizeValue * aspectRatio2),
|
||||||
|
height: resizeValue
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case 'both': {
|
||||||
|
return {
|
||||||
|
width: resizeValue,
|
||||||
|
height: resizeValue
|
||||||
|
};
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return {
|
||||||
|
width: originalWidth,
|
||||||
|
height: originalHeight
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get MIME type for output format
|
||||||
|
*/
|
||||||
|
function getMimeType(format: InitialValuesType['outputFormat']): string {
|
||||||
|
switch (format) {
|
||||||
|
case 'jpg':
|
||||||
|
return 'image/jpeg';
|
||||||
|
case 'png':
|
||||||
|
return 'image/png';
|
||||||
|
case 'webp':
|
||||||
|
return 'image/webp';
|
||||||
|
default:
|
||||||
|
return 'image/jpeg';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate output filename with correct extension
|
||||||
|
*/
|
||||||
|
function generateFileName(
|
||||||
|
originalName: string,
|
||||||
|
format: InitialValuesType['outputFormat']
|
||||||
|
): string {
|
||||||
|
const baseName = originalName.replace(/\.[^/.]+$/, '');
|
||||||
|
const extension = format === 'jpg' ? 'jpg' : format;
|
||||||
|
return `${baseName}.${extension}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate if file is a HEIC/HEIF image
|
||||||
|
*/
|
||||||
|
export function isHeicFile(file: File): boolean {
|
||||||
|
const heicTypes = ['image/heic', 'image/heif'];
|
||||||
|
return (
|
||||||
|
heicTypes.includes(file.type) ||
|
||||||
|
file.name.toLowerCase().endsWith('.heic') ||
|
||||||
|
file.name.toLowerCase().endsWith('.heif')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get file size in human readable format
|
||||||
|
*/
|
||||||
|
export function formatFileSize(bytes: number): string {
|
||||||
|
if (bytes === 0) return '0 Bytes';
|
||||||
|
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||||
|
}
|
||||||
14
src/pages/tools/image/generic/heic-converter/types.ts
Normal file
14
src/pages/tools/image/generic/heic-converter/types.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
export type InitialValuesType = {
|
||||||
|
outputFormat: 'jpg' | 'png' | 'webp';
|
||||||
|
quality: number;
|
||||||
|
preserveMetadata: boolean;
|
||||||
|
resizeMode: 'none' | 'width' | 'height' | 'both';
|
||||||
|
resizeValue: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ConversionResult = {
|
||||||
|
file: File;
|
||||||
|
originalSize: number;
|
||||||
|
convertedSize: number;
|
||||||
|
format: string;
|
||||||
|
};
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { tool as heicConverter } from './heic-converter/meta';
|
||||||
import { tool as resizeImage } from './resize/meta';
|
import { tool as resizeImage } from './resize/meta';
|
||||||
import { tool as compressImage } from './compress/meta';
|
import { tool as compressImage } from './compress/meta';
|
||||||
import { tool as changeColors } from './change-colors/meta';
|
import { tool as changeColors } from './change-colors/meta';
|
||||||
|
|
@ -10,6 +11,7 @@ import { tool as qrCodeGenerator } from './qr-code/meta';
|
||||||
import { tool as rotateImage } from './rotate/meta';
|
import { tool as rotateImage } from './rotate/meta';
|
||||||
import { tool as convertToJpg } from './convert-to-jpg/meta';
|
import { tool as convertToJpg } from './convert-to-jpg/meta';
|
||||||
import { tool as imageEditor } from './editor/meta';
|
import { tool as imageEditor } from './editor/meta';
|
||||||
|
|
||||||
export const imageGenericTools = [
|
export const imageGenericTools = [
|
||||||
imageEditor,
|
imageEditor,
|
||||||
resizeImage,
|
resizeImage,
|
||||||
|
|
@ -22,5 +24,6 @@ export const imageGenericTools = [
|
||||||
imageToText,
|
imageToText,
|
||||||
qrCodeGenerator,
|
qrCodeGenerator,
|
||||||
rotateImage,
|
rotateImage,
|
||||||
convertToJpg
|
convertToJpg,
|
||||||
|
heicConverter
|
||||||
];
|
];
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue