feat: svg resize

This commit is contained in:
Ibrahima G. Coulibaly
2025-04-02 04:05:00 +00:00
parent 08801116a7
commit 676359ed50
4 changed files with 248 additions and 172 deletions

167
.idea/workspace.xml generated
View File

@@ -4,33 +4,11 @@
<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: image resize init">
<change afterPath="$PROJECT_DIR$/src/pages/tools/image/generic/resize/service.ts" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/pages/tools/image/generic/resize/types.ts" afterDir="false" />
<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/pages/tools/image/generic/resize/index.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/tools/image/generic/resize/index.tsx" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -66,87 +44,87 @@
&quot;state&quot;: &quot;OPEN&quot;
}
}</component>
<component name="GitHubPullRequestState"><![CDATA[{
"prStates": [
<component name="GitHubPullRequestState">{
&quot;prStates&quot;: [
{
"id": {
"id": "PR_kwDOMJIfts51PkS9",
"number": 22
&quot;id&quot;: {
&quot;id&quot;: &quot;PR_kwDOMJIfts51PkS9&quot;,
&quot;number&quot;: 22
},
"lastSeen": 1741207144695
&quot;lastSeen&quot;: 1741207144695
},
{
"id": {
"id": "PR_kwDOMJIfts6NiNYl",
"number": 32
&quot;id&quot;: {
&quot;id&quot;: &quot;PR_kwDOMJIfts6NiNYl&quot;,
&quot;number&quot;: 32
},
"lastSeen": 1741209723869
&quot;lastSeen&quot;: 1741209723869
},
{
"id": {
"id": "PR_kwDOMJIfts6Nheyd",
"number": 31
&quot;id&quot;: {
&quot;id&quot;: &quot;PR_kwDOMJIfts6Nheyd&quot;,
&quot;number&quot;: 31
},
"lastSeen": 1741213371410
&quot;lastSeen&quot;: 1741213371410
},
{
"id": {
"id": "PR_kwDOMJIfts6NmRBs",
"number": 33
&quot;id&quot;: {
&quot;id&quot;: &quot;PR_kwDOMJIfts6NmRBs&quot;,
&quot;number&quot;: 33
},
"lastSeen": 1741282429036
&quot;lastSeen&quot;: 1741282429036
},
{
"id": {
"id": "PR_kwDOMJIfts5zyFTs",
"number": 15
&quot;id&quot;: {
&quot;id&quot;: &quot;PR_kwDOMJIfts5zyFTs&quot;,
&quot;number&quot;: 15
},
"lastSeen": 1741535540953
&quot;lastSeen&quot;: 1741535540953
},
{
"id": {
"id": "PR_kwDOMJIfts6QQB3c",
"number": 59
&quot;id&quot;: {
&quot;id&quot;: &quot;PR_kwDOMJIfts6QQB3c&quot;,
&quot;number&quot;: 59
},
"lastSeen": 1743018960900
&quot;lastSeen&quot;: 1743018960900
},
{
"id": {
"id": "PR_kwDOMJIfts6QMPEg",
"number": 58
&quot;id&quot;: {
&quot;id&quot;: &quot;PR_kwDOMJIfts6QMPEg&quot;,
&quot;number&quot;: 58
},
"lastSeen": 1743019452983
&quot;lastSeen&quot;: 1743019452983
},
{
"id": {
"id": "PR_kwDOMJIfts6QZvRI",
"number": 61
&quot;id&quot;: {
&quot;id&quot;: &quot;PR_kwDOMJIfts6QZvRI&quot;,
&quot;number&quot;: 61
},
"lastSeen": 1743103196866
&quot;lastSeen&quot;: 1743103196866
},
{
"id": {
"id": "PR_kwDOMJIfts6QqPrQ",
"number": 73
&quot;id&quot;: {
&quot;id&quot;: &quot;PR_kwDOMJIfts6QqPrQ&quot;,
&quot;number&quot;: 73
},
"lastSeen": 1743265865001
&quot;lastSeen&quot;: 1743265865001
},
{
"id": {
"id": "PR_kwDOMJIfts6Qp5nI",
"number": 72
&quot;id&quot;: {
&quot;id&quot;: &quot;PR_kwDOMJIfts6Qp5nI&quot;,
&quot;number&quot;: 72
},
"lastSeen": 1743338472110
&quot;lastSeen&quot;: 1743338472110
},
{
"id": {
"id": "PR_kwDOMJIfts6QsjlS",
"number": 76
&quot;id&quot;: {
&quot;id&quot;: &quot;PR_kwDOMJIfts6QsjlS&quot;,
&quot;number&quot;: 76
},
"lastSeen": 1743352150953
&quot;lastSeen&quot;: 1743352150953
}
]
}]]></component>
}</component>
<component name="GithubPullRequestsUISettings">{
&quot;selectedUrlAndAccountId&quot;: {
&quot;url&quot;: &quot;https://github.com/iib0011/omni-tools.git&quot;,
@@ -199,7 +177,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,22 +399,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>
<workItem from="1743556259185" duration="5197000" />
</task>
<task id="LOCAL-00129" summary="feat: json pretty">
<option name="closed" value="true" />
@@ -814,7 +777,23 @@
<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>
<option name="localTasksCounter" value="178" />
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
@@ -861,8 +840,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" />
@@ -886,7 +863,9 @@
<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" />
<option name="LAST_COMMIT_MESSAGE" value="feat: image resize init" />
</component>
<component name="XSLT-Support.FileAssociations.UIState">
<expand />

View File

@@ -9,8 +9,10 @@ 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 = {
const initialValues: InitialValuesType = {
resizeMethod: 'pixels' as 'pixels' | 'percentage',
dimensionType: 'width' as 'width' | 'height',
width: '800',
@@ -19,8 +21,6 @@ const initialValues = {
maintainAspectRatio: true
};
type InitialValuesType = typeof initialValues;
const validationSchema = Yup.object({
width: Yup.number().when('resizeMethod', {
is: 'pixels',
@@ -48,82 +48,9 @@ export default function ResizeImage({ title }: ToolComponentProps) {
const [input, setInput] = useState<File | null>(null);
const [result, setResult] = useState<File | null>(null);
const compute = (optionsValues: InitialValuesType, input: any) => {
const compute = async (optionsValues: InitialValuesType, input: any) => {
if (!input) return;
const {
resizeMethod,
dimensionType,
width,
height,
percentage,
maintainAspectRatio
} = optionsValues;
const processImage = async (file: File) => {
// Create canvas
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
if (ctx == null) return;
// 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
canvas.toBlob((blob) => {
if (blob) {
const newFile = new File([blob], file.name, {
type: outputType
});
setResult(newFile);
}
}, outputType);
};
processImage(input);
setResult(await processImage(input, optionsValues));
};
const getGroups: GetGroupsType<InitialValuesType> = ({

View File

@@ -0,0 +1,162 @@
import { InitialValuesType } from './types';
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
}
}
// 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);
});
};

View File

@@ -0,0 +1,8 @@
export type InitialValuesType = {
resizeMethod: 'pixels' | 'percentage';
dimensionType: 'width' | 'height';
width: string;
height: string;
percentage: string;
maintainAspectRatio: boolean;
};