mirror of
https://github.com/iib0011/omni-tools.git
synced 2025-09-18 13:39:31 +02:00
201
.idea/workspace.xml
generated
201
.idea/workspace.xml
generated
@@ -4,33 +4,9 @@
|
||||
<option name="autoReloadType" value="SELECTIVE" />
|
||||
</component>
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="b30e2810-c4c1-4aad-b134-794e52cc1c7d" name="Changes" comment="chore: compress video icon">
|
||||
<change afterPath="$PROJECT_DIR$/@types/theme.d.ts" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/public/assets/background-dark.png" afterDir="false" />
|
||||
<list default="true" id="b30e2810-c4c1-4aad-b134-794e52cc1c7d" name="Changes" comment="feat: gif resize">
|
||||
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/components/App.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/components/App.tsx" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/components/Hero.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/components/Hero.tsx" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/components/Navbar/index.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/components/Navbar/index.tsx" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/components/ToolHeader.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/components/ToolHeader.tsx" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/components/ToolLayout.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/components/ToolLayout.tsx" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/components/allTools/ToolCard.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/components/allTools/ToolCard.tsx" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/components/examples/ExampleCard.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/components/examples/ExampleCard.tsx" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/components/input/BaseFileInput.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/components/input/BaseFileInput.tsx" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/components/input/ToolFileInput.tsx" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/components/input/ToolTextInput.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/components/input/ToolTextInput.tsx" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/components/options/ColorSelector.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/components/options/ColorSelector.tsx" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/components/options/TextFieldWithDesc.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/components/options/TextFieldWithDesc.tsx" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/components/options/ToolOptions.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/components/options/ToolOptions.tsx" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/components/result/ToolFileResult.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/components/result/ToolFileResult.tsx" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/components/result/ToolTextResult.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/components/result/ToolTextResult.tsx" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/config/muiConfig.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/config/muiConfig.ts" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/pages/home/Categories.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/home/Categories.tsx" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/pages/home/index.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/home/index.tsx" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/pages/tools-by-category/index.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/tools-by-category/index.tsx" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/pages/tools/pdf/split-pdf/index.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/tools/pdf/split-pdf/index.tsx" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/pages/tools/video/compress/service.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/tools/video/compress/service.ts" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/pages/tools/video/gif/change-speed/index.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/tools/video/gif/change-speed/index.tsx" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/tsconfig.json" beforeDir="false" afterPath="$PROJECT_DIR$/tsconfig.json" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/tools/index.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/tools/index.ts" afterDir="false" />
|
||||
</list>
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
@@ -66,87 +42,87 @@
|
||||
"state": "OPEN"
|
||||
}
|
||||
}</component>
|
||||
<component name="GitHubPullRequestState"><![CDATA[{
|
||||
"prStates": [
|
||||
<component name="GitHubPullRequestState">{
|
||||
"prStates": [
|
||||
{
|
||||
"id": {
|
||||
"id": "PR_kwDOMJIfts51PkS9",
|
||||
"number": 22
|
||||
"id": {
|
||||
"id": "PR_kwDOMJIfts51PkS9",
|
||||
"number": 22
|
||||
},
|
||||
"lastSeen": 1741207144695
|
||||
"lastSeen": 1741207144695
|
||||
},
|
||||
{
|
||||
"id": {
|
||||
"id": "PR_kwDOMJIfts6NiNYl",
|
||||
"number": 32
|
||||
"id": {
|
||||
"id": "PR_kwDOMJIfts6NiNYl",
|
||||
"number": 32
|
||||
},
|
||||
"lastSeen": 1741209723869
|
||||
"lastSeen": 1741209723869
|
||||
},
|
||||
{
|
||||
"id": {
|
||||
"id": "PR_kwDOMJIfts6Nheyd",
|
||||
"number": 31
|
||||
"id": {
|
||||
"id": "PR_kwDOMJIfts6Nheyd",
|
||||
"number": 31
|
||||
},
|
||||
"lastSeen": 1741213371410
|
||||
"lastSeen": 1741213371410
|
||||
},
|
||||
{
|
||||
"id": {
|
||||
"id": "PR_kwDOMJIfts6NmRBs",
|
||||
"number": 33
|
||||
"id": {
|
||||
"id": "PR_kwDOMJIfts6NmRBs",
|
||||
"number": 33
|
||||
},
|
||||
"lastSeen": 1741282429036
|
||||
"lastSeen": 1741282429036
|
||||
},
|
||||
{
|
||||
"id": {
|
||||
"id": "PR_kwDOMJIfts5zyFTs",
|
||||
"number": 15
|
||||
"id": {
|
||||
"id": "PR_kwDOMJIfts5zyFTs",
|
||||
"number": 15
|
||||
},
|
||||
"lastSeen": 1741535540953
|
||||
"lastSeen": 1741535540953
|
||||
},
|
||||
{
|
||||
"id": {
|
||||
"id": "PR_kwDOMJIfts6QQB3c",
|
||||
"number": 59
|
||||
"id": {
|
||||
"id": "PR_kwDOMJIfts6QQB3c",
|
||||
"number": 59
|
||||
},
|
||||
"lastSeen": 1743018960900
|
||||
"lastSeen": 1743018960900
|
||||
},
|
||||
{
|
||||
"id": {
|
||||
"id": "PR_kwDOMJIfts6QMPEg",
|
||||
"number": 58
|
||||
"id": {
|
||||
"id": "PR_kwDOMJIfts6QMPEg",
|
||||
"number": 58
|
||||
},
|
||||
"lastSeen": 1743019452983
|
||||
"lastSeen": 1743019452983
|
||||
},
|
||||
{
|
||||
"id": {
|
||||
"id": "PR_kwDOMJIfts6QZvRI",
|
||||
"number": 61
|
||||
"id": {
|
||||
"id": "PR_kwDOMJIfts6QZvRI",
|
||||
"number": 61
|
||||
},
|
||||
"lastSeen": 1743103196866
|
||||
"lastSeen": 1743103196866
|
||||
},
|
||||
{
|
||||
"id": {
|
||||
"id": "PR_kwDOMJIfts6QqPrQ",
|
||||
"number": 73
|
||||
"id": {
|
||||
"id": "PR_kwDOMJIfts6QqPrQ",
|
||||
"number": 73
|
||||
},
|
||||
"lastSeen": 1743265865001
|
||||
"lastSeen": 1743265865001
|
||||
},
|
||||
{
|
||||
"id": {
|
||||
"id": "PR_kwDOMJIfts6Qp5nI",
|
||||
"number": 72
|
||||
"id": {
|
||||
"id": "PR_kwDOMJIfts6Qp5nI",
|
||||
"number": 72
|
||||
},
|
||||
"lastSeen": 1743338472110
|
||||
"lastSeen": 1743338472110
|
||||
},
|
||||
{
|
||||
"id": {
|
||||
"id": "PR_kwDOMJIfts6QsjlS",
|
||||
"number": 76
|
||||
"id": {
|
||||
"id": "PR_kwDOMJIfts6QsjlS",
|
||||
"number": 76
|
||||
},
|
||||
"lastSeen": 1743352150953
|
||||
"lastSeen": 1743352150953
|
||||
}
|
||||
]
|
||||
}]]></component>
|
||||
}</component>
|
||||
<component name="GithubPullRequestsUISettings">{
|
||||
"selectedUrlAndAccountId": {
|
||||
"url": "https://github.com/iib0011/omni-tools.git",
|
||||
@@ -199,7 +175,7 @@
|
||||
"Vitest.replaceText function (regexp mode).should return the original text when passed an invalid regexp.executor": "Run",
|
||||
"Vitest.replaceText function.executor": "Run",
|
||||
"Vitest.timeBetweenDates.executor": "Run",
|
||||
"git-widget-placeholder": "dark-mode",
|
||||
"git-widget-placeholder": "image-resize",
|
||||
"ignore.virus.scanning.warn.message": "true",
|
||||
"kotlin-language-version-configured": "true",
|
||||
"last_opened_file_path": "C:/Users/Ibrahima/IdeaProjects/omni-tools/@types",
|
||||
@@ -421,38 +397,7 @@
|
||||
<workItem from="1743047367993" duration="986000" />
|
||||
<workItem from="1743103182313" duration="4264000" />
|
||||
<workItem from="1743348610793" duration="21855000" />
|
||||
</task>
|
||||
<task id="LOCAL-00127" summary="chore: show tooloptions in example">
|
||||
<option name="closed" value="true" />
|
||||
<created>1740619610168</created>
|
||||
<option name="number" value="00127" />
|
||||
<option name="presentableId" value="LOCAL-00127" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1740619610169</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00128" summary="refact: examples">
|
||||
<option name="closed" value="true" />
|
||||
<created>1740620866551</created>
|
||||
<option name="number" value="00128" />
|
||||
<option name="presentableId" value="LOCAL-00128" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1740620866551</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00129" summary="feat: json pretty">
|
||||
<option name="closed" value="true" />
|
||||
<created>1740661424202</created>
|
||||
<option name="number" value="00129" />
|
||||
<option name="presentableId" value="LOCAL-00129" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1740661424202</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00130" summary="feat: json pretty">
|
||||
<option name="closed" value="true" />
|
||||
<created>1740661540908</created>
|
||||
<option name="number" value="00130" />
|
||||
<option name="presentableId" value="LOCAL-00130" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1740661540908</updated>
|
||||
<workItem from="1743556259185" duration="7150000" />
|
||||
</task>
|
||||
<task id="LOCAL-00131" summary="style: tool categories">
|
||||
<option name="closed" value="true" />
|
||||
@@ -814,7 +759,39 @@
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1743355166426</updated>
|
||||
</task>
|
||||
<option name="localTasksCounter" value="176" />
|
||||
<task id="LOCAL-00176" summary="chore: tool description">
|
||||
<option name="closed" value="true" />
|
||||
<created>1743560690570</created>
|
||||
<option name="number" value="00176" />
|
||||
<option name="presentableId" value="LOCAL-00176" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1743560690571</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00177" summary="feat: image resize init">
|
||||
<option name="closed" value="true" />
|
||||
<created>1743565606951</created>
|
||||
<option name="number" value="00177" />
|
||||
<option name="presentableId" value="LOCAL-00177" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1743565606951</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00178" summary="feat: svg resize">
|
||||
<option name="closed" value="true" />
|
||||
<created>1743566704552</created>
|
||||
<option name="number" value="00178" />
|
||||
<option name="presentableId" value="LOCAL-00178" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1743566704552</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00179" summary="feat: gif resize">
|
||||
<option name="closed" value="true" />
|
||||
<created>1743567906528</created>
|
||||
<option name="number" value="00179" />
|
||||
<option name="presentableId" value="LOCAL-00179" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1743567906528</updated>
|
||||
</task>
|
||||
<option name="localTasksCounter" value="180" />
|
||||
<servers />
|
||||
</component>
|
||||
<component name="TypeScriptGeneratedFilesManager">
|
||||
@@ -861,10 +838,6 @@
|
||||
<option name="CHECK_CODE_SMELLS_BEFORE_PROJECT_COMMIT" value="false" />
|
||||
<option name="CHECK_NEW_TODO" value="false" />
|
||||
<option name="ADD_EXTERNAL_FILES_SILENTLY" value="true" />
|
||||
<MESSAGE value="style: tools height" />
|
||||
<MESSAGE value="chore: update meta" />
|
||||
<MESSAGE value="feat: change pgn opacity" />
|
||||
<MESSAGE value="feat: crop png" />
|
||||
<MESSAGE value="chore: remove unnecessary files" />
|
||||
<MESSAGE value="refactor: validateJson" />
|
||||
<MESSAGE value="feat: missing tools" />
|
||||
@@ -886,7 +859,11 @@
|
||||
<MESSAGE value="fix: typos" />
|
||||
<MESSAGE value="feat: compress video" />
|
||||
<MESSAGE value="chore: compress video icon" />
|
||||
<option name="LAST_COMMIT_MESSAGE" value="chore: compress video icon" />
|
||||
<MESSAGE value="chore: tool description" />
|
||||
<MESSAGE value="feat: image resize init" />
|
||||
<MESSAGE value="feat: svg resize" />
|
||||
<MESSAGE value="feat: gif resize" />
|
||||
<option name="LAST_COMMIT_MESSAGE" value="feat: gif resize" />
|
||||
</component>
|
||||
<component name="XSLT-Support.FileAssociations.UIState">
|
||||
<expand />
|
||||
|
@@ -5,6 +5,7 @@ import { capitalizeFirstLetter } from '../utils/string';
|
||||
import Grid from '@mui/material/Grid';
|
||||
import { Icon, IconifyIcon } from '@iconify/react';
|
||||
import { categoriesColors } from '../config/uiConfig';
|
||||
import { getToolsByCategory } from '@tools/index';
|
||||
|
||||
const StyledButton = styled(Button)(({ theme }) => ({
|
||||
backgroundColor: 'white',
|
||||
@@ -70,7 +71,9 @@ export default function ToolHeader({
|
||||
items={[
|
||||
{ title: 'All tools', link: '/' },
|
||||
{
|
||||
title: capitalizeFirstLetter(type),
|
||||
title: getToolsByCategory().find(
|
||||
(category) => category.type === type
|
||||
)!.rawTitle,
|
||||
link: '/categories/' + type
|
||||
},
|
||||
{ title }
|
||||
|
@@ -53,7 +53,10 @@ export default function ToolLayout({
|
||||
{children}
|
||||
<Separator backgroundColor="#5581b5" margin="50px" />
|
||||
<AllTools
|
||||
title={`All ${capitalizeFirstLetter(type)} tools`}
|
||||
title={`All ${capitalizeFirstLetter(
|
||||
getToolsByCategory().find((category) => category.type === type)!
|
||||
.rawTitle
|
||||
)} tools`}
|
||||
toolCards={otherCategoryTools}
|
||||
/>
|
||||
</Box>
|
||||
|
@@ -43,10 +43,11 @@ export default function Home() {
|
||||
<IconButton onClick={() => navigate('/')}>
|
||||
<ArrowBackIcon color={'primary'} />
|
||||
</IconButton>
|
||||
<Typography
|
||||
fontSize={22}
|
||||
color={theme.palette.primary.main}
|
||||
>{`All ${capitalizeFirstLetter(categoryName)} Tools`}</Typography>
|
||||
<Typography fontSize={22} color={theme.palette.primary.main}>{`All ${
|
||||
getToolsByCategory().find(
|
||||
(category) => category.type === categoryName
|
||||
)!.rawTitle
|
||||
} Tools`}</Typography>
|
||||
</Stack>
|
||||
<Grid container spacing={2} mt={2}>
|
||||
{getToolsByCategory()
|
||||
|
3
src/pages/tools/image/generic/index.ts
Normal file
3
src/pages/tools/image/generic/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { tool as resizeImage } from './resize/meta';
|
||||
|
||||
export const imageGenericTools = [resizeImage];
|
203
src/pages/tools/image/generic/resize/index.tsx
Normal file
203
src/pages/tools/image/generic/resize/index.tsx
Normal file
@@ -0,0 +1,203 @@
|
||||
import { Box } from '@mui/material';
|
||||
import React, { useState } from 'react';
|
||||
import * as Yup from 'yup';
|
||||
import ToolImageInput from '@components/input/ToolImageInput';
|
||||
import ToolFileResult from '@components/result/ToolFileResult';
|
||||
import { GetGroupsType } from '@components/options/ToolOptions';
|
||||
import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
|
||||
import ToolContent from '@components/ToolContent';
|
||||
import { ToolComponentProps } from '@tools/defineTool';
|
||||
import SimpleRadio from '@components/options/SimpleRadio';
|
||||
import CheckboxWithDesc from '@components/options/CheckboxWithDesc';
|
||||
import { processImage } from './service';
|
||||
import { InitialValuesType } from './types';
|
||||
|
||||
const initialValues: InitialValuesType = {
|
||||
resizeMethod: 'pixels' as 'pixels' | 'percentage',
|
||||
dimensionType: 'width' as 'width' | 'height',
|
||||
width: '800',
|
||||
height: '600',
|
||||
percentage: '50',
|
||||
maintainAspectRatio: true
|
||||
};
|
||||
|
||||
const validationSchema = Yup.object({
|
||||
width: Yup.number().when('resizeMethod', {
|
||||
is: 'pixels',
|
||||
then: (schema) =>
|
||||
schema.min(1, 'Width must be at least 1px').required('Width is required')
|
||||
}),
|
||||
height: Yup.number().when('resizeMethod', {
|
||||
is: 'pixels',
|
||||
then: (schema) =>
|
||||
schema
|
||||
.min(1, 'Height must be at least 1px')
|
||||
.required('Height is required')
|
||||
}),
|
||||
percentage: Yup.number().when('resizeMethod', {
|
||||
is: 'percentage',
|
||||
then: (schema) =>
|
||||
schema
|
||||
.min(1, 'Percentage must be at least 1%')
|
||||
.max(1000, 'Percentage must be at most 1000%')
|
||||
.required('Percentage is required')
|
||||
})
|
||||
});
|
||||
|
||||
export default function ResizeImage({ title }: ToolComponentProps) {
|
||||
const [input, setInput] = useState<File | null>(null);
|
||||
const [result, setResult] = useState<File | null>(null);
|
||||
|
||||
const compute = async (optionsValues: InitialValuesType, input: any) => {
|
||||
if (!input) return;
|
||||
setResult(await processImage(input, optionsValues));
|
||||
};
|
||||
|
||||
const getGroups: GetGroupsType<InitialValuesType> = ({
|
||||
values,
|
||||
updateField
|
||||
}) => [
|
||||
{
|
||||
title: 'Resize Method',
|
||||
component: (
|
||||
<Box>
|
||||
<SimpleRadio
|
||||
onClick={() => updateField('resizeMethod', 'pixels')}
|
||||
checked={values.resizeMethod === 'pixels'}
|
||||
description={'Resize by specifying dimensions in pixels.'}
|
||||
title={'Resize by Pixels'}
|
||||
/>
|
||||
<SimpleRadio
|
||||
onClick={() => updateField('resizeMethod', 'percentage')}
|
||||
checked={values.resizeMethod === 'percentage'}
|
||||
description={
|
||||
'Resize by specifying a percentage of the original size.'
|
||||
}
|
||||
title={'Resize by Percentage'}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
},
|
||||
...(values.resizeMethod === 'pixels'
|
||||
? [
|
||||
{
|
||||
title: 'Dimension Type',
|
||||
component: (
|
||||
<Box>
|
||||
<CheckboxWithDesc
|
||||
checked={values.maintainAspectRatio}
|
||||
onChange={(value) =>
|
||||
updateField('maintainAspectRatio', value)
|
||||
}
|
||||
description={
|
||||
'Maintain the original aspect ratio of the image.'
|
||||
}
|
||||
title={'Maintain Aspect Ratio'}
|
||||
/>
|
||||
{values.maintainAspectRatio && (
|
||||
<Box>
|
||||
<SimpleRadio
|
||||
onClick={() => updateField('dimensionType', 'width')}
|
||||
checked={values.dimensionType === 'width'}
|
||||
description={
|
||||
'Specify the width in pixels and calculate height based on aspect ratio.'
|
||||
}
|
||||
title={'Set Width'}
|
||||
/>
|
||||
<SimpleRadio
|
||||
onClick={() => updateField('dimensionType', 'height')}
|
||||
checked={values.dimensionType === 'height'}
|
||||
description={
|
||||
'Specify the height in pixels and calculate width based on aspect ratio.'
|
||||
}
|
||||
title={'Set Height'}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
<TextFieldWithDesc
|
||||
value={values.width}
|
||||
onOwnChange={(val) => updateField('width', val)}
|
||||
description={'Width (in pixels)'}
|
||||
disabled={
|
||||
values.maintainAspectRatio &&
|
||||
values.dimensionType === 'height'
|
||||
}
|
||||
inputProps={{
|
||||
'data-testid': 'width-input',
|
||||
type: 'number',
|
||||
min: 1
|
||||
}}
|
||||
/>
|
||||
<TextFieldWithDesc
|
||||
value={values.height}
|
||||
onOwnChange={(val) => updateField('height', val)}
|
||||
description={'Height (in pixels)'}
|
||||
disabled={
|
||||
values.maintainAspectRatio &&
|
||||
values.dimensionType === 'width'
|
||||
}
|
||||
inputProps={{
|
||||
'data-testid': 'height-input',
|
||||
type: 'number',
|
||||
min: 1
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
]
|
||||
: [
|
||||
{
|
||||
title: 'Percentage',
|
||||
component: (
|
||||
<Box>
|
||||
<TextFieldWithDesc
|
||||
value={values.percentage}
|
||||
onOwnChange={(val) => updateField('percentage', val)}
|
||||
description={
|
||||
'Percentage of original size (e.g., 50 for half size, 200 for double size)'
|
||||
}
|
||||
inputProps={{
|
||||
'data-testid': 'percentage-input',
|
||||
type: 'number',
|
||||
min: 1,
|
||||
max: 1000
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
])
|
||||
];
|
||||
|
||||
return (
|
||||
<ToolContent
|
||||
title={title}
|
||||
initialValues={initialValues}
|
||||
getGroups={getGroups}
|
||||
compute={compute}
|
||||
input={input}
|
||||
validationSchema={validationSchema}
|
||||
inputComponent={
|
||||
<ToolImageInput
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
accept={['image/jpeg', 'image/png', 'image/svg+xml', 'image/gif']}
|
||||
title={'Input Image'}
|
||||
/>
|
||||
}
|
||||
resultComponent={
|
||||
<ToolFileResult
|
||||
title={'Resized Image'}
|
||||
value={result}
|
||||
extension={input?.name.split('.').pop() || 'png'}
|
||||
/>
|
||||
}
|
||||
toolInfo={{
|
||||
title: 'Resize Image',
|
||||
description:
|
||||
'This tool allows you to resize JPG, PNG, SVG, or GIF images. You can resize by specifying dimensions in pixels or by percentage, with options to maintain the original aspect ratio.'
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
22
src/pages/tools/image/generic/resize/meta.ts
Normal file
22
src/pages/tools/image/generic/resize/meta.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('image-generic', {
|
||||
name: 'Resize Image',
|
||||
path: 'resize',
|
||||
icon: 'mdi:resize', // Iconify icon as a string
|
||||
description:
|
||||
'Resize JPG, PNG, SVG or GIF images by pixels or percentage while maintaining aspect ratio or not.',
|
||||
shortDescription: 'Resize images easily.',
|
||||
keywords: [
|
||||
'resize',
|
||||
'image',
|
||||
'scale',
|
||||
'jpg',
|
||||
'png',
|
||||
'svg',
|
||||
'gif',
|
||||
'dimensions'
|
||||
],
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
218
src/pages/tools/image/generic/resize/service.ts
Normal file
218
src/pages/tools/image/generic/resize/service.ts
Normal file
@@ -0,0 +1,218 @@
|
||||
import { InitialValuesType } from './types';
|
||||
import { FFmpeg } from '@ffmpeg/ffmpeg';
|
||||
import { fetchFile, toBlobURL } from '@ffmpeg/util';
|
||||
|
||||
export const processImage = async (
|
||||
file: File,
|
||||
options: InitialValuesType
|
||||
): Promise<File | null> => {
|
||||
const {
|
||||
width,
|
||||
height,
|
||||
resizeMethod,
|
||||
percentage,
|
||||
dimensionType,
|
||||
maintainAspectRatio
|
||||
} = options;
|
||||
if (file.type === 'image/svg+xml') {
|
||||
try {
|
||||
// Read the SVG file
|
||||
const fileText = await file.text();
|
||||
const parser = new DOMParser();
|
||||
const svgDoc = parser.parseFromString(fileText, 'image/svg+xml');
|
||||
const svgElement = svgDoc.documentElement;
|
||||
|
||||
// Get original dimensions
|
||||
const viewBox = svgElement.getAttribute('viewBox');
|
||||
let originalWidth: string | number | null =
|
||||
svgElement.getAttribute('width');
|
||||
let originalHeight: string | number | null =
|
||||
svgElement.getAttribute('height');
|
||||
|
||||
// Parse viewBox if available and width/height are not explicitly set
|
||||
let viewBoxValues = null;
|
||||
if (viewBox) {
|
||||
viewBoxValues = viewBox.split(' ').map(Number);
|
||||
}
|
||||
|
||||
// Determine original dimensions from viewBox if not explicitly set
|
||||
if (!originalWidth && viewBoxValues && viewBoxValues.length === 4) {
|
||||
originalWidth = String(viewBoxValues[2]);
|
||||
}
|
||||
if (!originalHeight && viewBoxValues && viewBoxValues.length === 4) {
|
||||
originalHeight = String(viewBoxValues[3]);
|
||||
}
|
||||
|
||||
// Default dimensions if still not available
|
||||
originalWidth = originalWidth ? parseFloat(originalWidth) : 300;
|
||||
originalHeight = originalHeight ? parseFloat(originalHeight) : 150;
|
||||
|
||||
// Calculate new dimensions
|
||||
let newWidth = originalWidth;
|
||||
let newHeight = originalHeight;
|
||||
|
||||
if (resizeMethod === 'pixels') {
|
||||
if (dimensionType === 'width') {
|
||||
newWidth = parseInt(width);
|
||||
if (maintainAspectRatio) {
|
||||
newHeight = Math.round((newWidth / originalWidth) * originalHeight);
|
||||
} else {
|
||||
newHeight = parseInt(height);
|
||||
}
|
||||
} else {
|
||||
// height
|
||||
newHeight = parseInt(height);
|
||||
if (maintainAspectRatio) {
|
||||
newWidth = Math.round((newHeight / originalHeight) * originalWidth);
|
||||
} else {
|
||||
newWidth = parseInt(width);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// percentage
|
||||
const scale = parseInt(percentage) / 100;
|
||||
newWidth = Math.round(originalWidth * scale);
|
||||
newHeight = Math.round(originalHeight * scale);
|
||||
}
|
||||
|
||||
// Update SVG attributes
|
||||
svgElement.setAttribute('width', String(newWidth));
|
||||
svgElement.setAttribute('height', String(newHeight));
|
||||
|
||||
// If viewBox isn't already set, add it to preserve scaling
|
||||
if (!viewBox) {
|
||||
svgElement.setAttribute(
|
||||
'viewBox',
|
||||
`0 0 ${originalWidth} ${originalHeight}`
|
||||
);
|
||||
}
|
||||
|
||||
// Serialize the modified SVG document
|
||||
const serializer = new XMLSerializer();
|
||||
const svgString = serializer.serializeToString(svgDoc);
|
||||
|
||||
// Create a new file
|
||||
return new File([svgString], file.name, {
|
||||
type: 'image/svg+xml'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error processing SVG:', error);
|
||||
// Fall back to canvas method if SVG processing fails
|
||||
}
|
||||
} else if (file.type === 'image/gif') {
|
||||
try {
|
||||
const ffmpeg = new FFmpeg();
|
||||
|
||||
await ffmpeg.load({
|
||||
wasmURL:
|
||||
'https://cdn.jsdelivr.net/npm/@ffmpeg/core@0.12.9/dist/esm/ffmpeg-core.wasm'
|
||||
});
|
||||
|
||||
// Write the input file to memory
|
||||
await ffmpeg.writeFile('input.gif', await fetchFile(file));
|
||||
|
||||
// Calculate new dimensions
|
||||
let newWidth = 0;
|
||||
let newHeight = 0;
|
||||
let scaleFilter = '';
|
||||
|
||||
if (resizeMethod === 'pixels') {
|
||||
if (dimensionType === 'width') {
|
||||
newWidth = parseInt(width);
|
||||
if (maintainAspectRatio) {
|
||||
scaleFilter = `scale=${newWidth}:-1`;
|
||||
} else {
|
||||
newHeight = parseInt(height);
|
||||
scaleFilter = `scale=${newWidth}:${newHeight}`;
|
||||
}
|
||||
} else {
|
||||
// height
|
||||
newHeight = parseInt(height);
|
||||
if (maintainAspectRatio) {
|
||||
scaleFilter = `scale=-1:${newHeight}`;
|
||||
} else {
|
||||
newWidth = parseInt(width);
|
||||
scaleFilter = `scale=${newWidth}:${newHeight}`;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// percentage
|
||||
const scale = parseInt(percentage) / 100;
|
||||
scaleFilter = `scale=iw*${scale}:ih*${scale}`;
|
||||
}
|
||||
|
||||
// Run FFmpeg command
|
||||
await ffmpeg.exec(['-i', 'input.gif', '-vf', scaleFilter, 'output.gif']);
|
||||
|
||||
// Read the output file
|
||||
const data = await ffmpeg.readFile('output.gif');
|
||||
|
||||
// Create a new File object
|
||||
return new File([data], file.name, { type: 'image/gif' });
|
||||
} catch (error) {
|
||||
console.error('Error processing GIF with FFmpeg:', error);
|
||||
// Fall back to canvas method if FFmpeg processing fails
|
||||
}
|
||||
}
|
||||
// Create canvas
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (ctx == null) return null;
|
||||
|
||||
// Load image
|
||||
const img = new Image();
|
||||
img.src = URL.createObjectURL(file);
|
||||
await img.decode();
|
||||
|
||||
// Calculate new dimensions
|
||||
let newWidth = img.width;
|
||||
let newHeight = img.height;
|
||||
|
||||
if (resizeMethod === 'pixels') {
|
||||
if (dimensionType === 'width') {
|
||||
newWidth = parseInt(width);
|
||||
if (maintainAspectRatio) {
|
||||
newHeight = Math.round((newWidth / img.width) * img.height);
|
||||
} else {
|
||||
newHeight = parseInt(height);
|
||||
}
|
||||
} else {
|
||||
// height
|
||||
newHeight = parseInt(height);
|
||||
if (maintainAspectRatio) {
|
||||
newWidth = Math.round((newHeight / img.height) * img.width);
|
||||
} else {
|
||||
newWidth = parseInt(width);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// percentage
|
||||
const scale = parseInt(percentage) / 100;
|
||||
newWidth = Math.round(img.width * scale);
|
||||
newHeight = Math.round(img.height * scale);
|
||||
}
|
||||
|
||||
// Set canvas dimensions
|
||||
canvas.width = newWidth;
|
||||
canvas.height = newHeight;
|
||||
|
||||
// Draw resized image
|
||||
ctx.drawImage(img, 0, 0, newWidth, newHeight);
|
||||
|
||||
// Determine output type based on input file
|
||||
let outputType = 'image/png';
|
||||
if (file.type) {
|
||||
outputType = file.type;
|
||||
}
|
||||
|
||||
// Convert canvas to blob and create file
|
||||
return new Promise((resolve) => {
|
||||
canvas.toBlob((blob) => {
|
||||
if (blob) {
|
||||
resolve(new File([blob], file.name, { type: outputType }));
|
||||
} else {
|
||||
resolve(null);
|
||||
}
|
||||
}, outputType);
|
||||
});
|
||||
};
|
8
src/pages/tools/image/generic/resize/types.ts
Normal file
8
src/pages/tools/image/generic/resize/types.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export type InitialValuesType = {
|
||||
resizeMethod: 'pixels' | 'percentage';
|
||||
dimensionType: 'width' | 'height';
|
||||
width: string;
|
||||
height: string;
|
||||
percentage: string;
|
||||
maintainAspectRatio: boolean;
|
||||
};
|
@@ -1,3 +1,4 @@
|
||||
import { pngTools } from './png';
|
||||
import { imageGenericTools } from './generic';
|
||||
|
||||
export const imageTools = [...pngTools];
|
||||
export const imageTools = [...imageGenericTools, ...pngTools];
|
||||
|
@@ -8,7 +8,7 @@ export const tool = defineTool('png', {
|
||||
icon: 'material-symbols-light:compress',
|
||||
description:
|
||||
'This is a program that compresses PNG pictures. As soon as you paste your PNG picture in the input area, the program will compress it and show the result in the output area. In the options, you can adjust the compression level, as well as find the old and new picture file sizes.',
|
||||
shortDescription: 'Quicly compress a PNG',
|
||||
shortDescription: 'Quickly compress a PNG',
|
||||
keywords: ['compress', 'png'],
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
|
@@ -23,8 +23,8 @@ export type ToolCategory =
|
||||
| 'json'
|
||||
| 'time'
|
||||
| 'csv'
|
||||
| 'time'
|
||||
| 'pdf';
|
||||
| 'pdf'
|
||||
| 'image-generic';
|
||||
|
||||
export interface DefinedTool {
|
||||
type: ToolCategory;
|
||||
|
@@ -12,6 +12,19 @@ import { timeTools } from '../pages/tools/time';
|
||||
import { IconifyIcon } from '@iconify/react';
|
||||
import { pdfTools } from '../pages/tools/pdf';
|
||||
|
||||
const toolCategoriesOrder: ToolCategory[] = [
|
||||
'png',
|
||||
'string',
|
||||
'json',
|
||||
'pdf',
|
||||
'list',
|
||||
'csv',
|
||||
'video',
|
||||
'number',
|
||||
'gif',
|
||||
'time',
|
||||
'image-generic'
|
||||
];
|
||||
export const tools: DefinedTool[] = [
|
||||
...imageTools,
|
||||
...stringTools,
|
||||
@@ -95,6 +108,13 @@ const categoriesConfig: {
|
||||
icon: 'fluent-mdl2:date-time',
|
||||
value:
|
||||
'Tools for working with time and date – draw clocks and calendars, generate time and date sequences, calculate average time, convert between time zones, and much more.'
|
||||
},
|
||||
{
|
||||
type: 'image-generic',
|
||||
title: 'Image',
|
||||
icon: 'material-symbols-light:image-outline-rounded',
|
||||
value:
|
||||
'Tools for working with pictures – compress, resize, crop, convert to JPG, rotate, remove background and much more.'
|
||||
}
|
||||
];
|
||||
// use for changelogs
|
||||
@@ -123,20 +143,22 @@ export const filterTools = (
|
||||
|
||||
export const getToolsByCategory = (): {
|
||||
title: string;
|
||||
rawTitle: string;
|
||||
description: string;
|
||||
icon: IconifyIcon | string;
|
||||
type: string;
|
||||
type: ToolCategory;
|
||||
example: { title: string; path: string };
|
||||
tools: DefinedTool[];
|
||||
}[] => {
|
||||
const groupedByType: Partial<Record<ToolCategory, DefinedTool[]>> =
|
||||
Object.groupBy(tools, ({ type }) => type);
|
||||
return (Object.entries(groupedByType) as Entries<typeof groupedByType>).map(
|
||||
([type, tools]) => {
|
||||
return (Object.entries(groupedByType) as Entries<typeof groupedByType>)
|
||||
.map(([type, tools]) => {
|
||||
const categoryConfig = categoriesConfig.find(
|
||||
(config) => config.type === type
|
||||
);
|
||||
return {
|
||||
rawTitle: categoryConfig?.title ?? capitalizeFirstLetter(type),
|
||||
title: `${categoryConfig?.title ?? capitalizeFirstLetter(type)} Tools`,
|
||||
description: categoryConfig?.value ?? '',
|
||||
type,
|
||||
@@ -146,6 +168,10 @@ export const getToolsByCategory = (): {
|
||||
? { title: tools[0].name, path: tools[0].path }
|
||||
: { title: '', path: '' }
|
||||
};
|
||||
}
|
||||
);
|
||||
})
|
||||
.sort(
|
||||
(a, b) =>
|
||||
toolCategoriesOrder.indexOf(a.type) -
|
||||
toolCategoriesOrder.indexOf(b.type)
|
||||
);
|
||||
};
|
||||
|
Reference in New Issue
Block a user