Skip to content

Commit 04d3fa2

Browse files
committed
feat(new tool) image-resizer
1 parent 0b1b98f commit 04d3fa2

File tree

5 files changed

+222
-1
lines changed

5 files changed

+222
-1
lines changed

components.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ declare module '@vue/runtime-core' {
101101
IconMdiSearch: typeof import('~icons/mdi/search')['default']
102102
IconMdiTranslate: typeof import('~icons/mdi/translate')['default']
103103
IconMdiTriangleDown: typeof import('~icons/mdi/triangle-down')['default']
104+
ImageResizer: typeof import('./src/tools/image-resizer/image-resizer.vue')['default']
104105
InputCopyable: typeof import('./src/components/InputCopyable.vue')['default']
105106
IntegerBaseConverter: typeof import('./src/tools/integer-base-converter/integer-base-converter.vue')['default']
106107
Ipv4AddressConverter: typeof import('./src/tools/ipv4-address-converter/ipv4-address-converter.vue')['default']

locales/en.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,3 +392,7 @@ tools:
392392
text-to-binary:
393393
title: Text to ASCII binary
394394
description: Convert text to its ASCII binary representation and vice-versa.
395+
396+
image-resizer:
397+
title: Image resizer
398+
description: Convert the width and height of an image file, preview, and download it in a desired format (.jpg, .jpeg, .png, .bmp, .ico, .svg)
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
<script setup lang="ts">
2+
import { ref, watch } from 'vue';
3+
4+
// State variables
5+
const imageFile = ref<File | null>(null);
6+
const imageUrl = ref<string | null>(null);
7+
const originalImageUrl = ref<string | null>(null); // To store original image data
8+
const imageWidth = ref(500); // Default width
9+
const imageHeight = ref(350); // Default height
10+
const originalImageWidth = ref<number | null>(null); // Store original image width
11+
const originalImageHeight = ref<number | null>(null); // Store original image height
12+
const resizedImageUrl = ref<string | null>(null);
13+
14+
// Watch width to trigger resizing
15+
watch(imageWidth, () => {
16+
resizeImage();
17+
});
18+
19+
// Watch height to trigger resizing
20+
watch(imageHeight, () => {
21+
resizeImage();
22+
});
23+
24+
// Handle file upload
25+
async function handleFileUpload(uploadedFile: File) {
26+
if (!uploadedFile) {
27+
return;
28+
}
29+
30+
imageFile.value = uploadedFile;
31+
32+
try {
33+
// Read the file as a Data URL
34+
const reader = new FileReader();
35+
const fileDataUrl = await new Promise<string>((resolve, reject) => {
36+
reader.onload = () => resolve(reader.result as string);
37+
reader.onerror = error => reject(error);
38+
reader.readAsDataURL(uploadedFile);
39+
});
40+
41+
imageUrl.value = fileDataUrl; // Preview image
42+
originalImageUrl.value = fileDataUrl; // Store original image for resizing
43+
resizedImageUrl.value = null; // Clear previous resized image
44+
45+
// Create an image to get original dimensions
46+
const img = new Image();
47+
img.src = fileDataUrl;
48+
49+
await new Promise<void>((resolve) => {
50+
img.onload = () => {
51+
// Set original image dimensions
52+
originalImageWidth.value = img.naturalWidth;
53+
originalImageHeight.value = img.naturalHeight;
54+
55+
// Automatically resize if width and height are set
56+
if (imageWidth.value > 0 && imageHeight.value > 0) {
57+
resizeImage();
58+
}
59+
60+
resolve();
61+
};
62+
});
63+
}
64+
catch (error) {
65+
console.error('Error reading file:', error);
66+
}
67+
}
68+
69+
// Function to resize the image
70+
async function resizeImage() {
71+
if (!originalImageUrl.value) {
72+
return; // Ensure there's an original image to work with
73+
}
74+
75+
const img = new Image();
76+
img.src = originalImageUrl.value; // Use the original image for resizing
77+
78+
img.onload = () => {
79+
const canvas = document.createElement('canvas');
80+
canvas.width = imageWidth.value;
81+
canvas.height = imageHeight.value;
82+
83+
const ctx = canvas.getContext('2d');
84+
ctx?.drawImage(img, 0, 0, imageWidth.value, imageHeight.value);
85+
resizedImageUrl.value = canvas.toDataURL('image/png');
86+
};
87+
}
88+
89+
// Function to download resized image
90+
function downloadImage(format: string) {
91+
if (!resizedImageUrl.value || !imageFile.value) {
92+
return;
93+
}
94+
95+
const originalFilename = imageFile.value.name.replace(/\.[^/.]+$/, ''); // Remove file extension
96+
const newFilename = `${originalFilename}-${imageWidth.value}x${imageHeight.value}.${format}`;
97+
const link = document.createElement('a');
98+
link.href = resizedImageUrl.value;
99+
link.download = newFilename;
100+
link.click();
101+
}
102+
</script>
103+
104+
<template>
105+
<n-card>
106+
<div>
107+
<!-- File input -->
108+
<c-file-upload mb-2 accept=".jpg,.jpeg,.png,.bmp,.ico,.svg" title="Drag and drop a .jpg, .jpeg, .png, .bmp, .ico, .svg file here" @file-upload="handleFileUpload" />
109+
110+
<!-- Original image dimensions -->
111+
<div v-if="originalImageWidth && originalImageHeight">
112+
<p>Original Image Dimensions: {{ originalImageWidth }}x{{ originalImageHeight }}px</p>
113+
</div>
114+
115+
<!-- Width and height inputs -->
116+
<div class="input-group">
117+
<label for="widthInput">Width (px):</label>
118+
<n-input-number id="widthInput" v-model:value="imageWidth" placeholder="Width (px)" mb-3 />
119+
</div>
120+
121+
<div class="input-group">
122+
<label for="heightInput">Height (px):</label>
123+
<n-input-number id="heightInput" v-model:value="imageHeight" placeholder="Height (px)" mb-1 />
124+
</div>
125+
</div>
126+
127+
<!-- Image preview -->
128+
<div v-if="resizedImageUrl" class="image-container" style="text-align: center; margin-top: 20px;">
129+
<div class="image-wrapper">
130+
<img :src="resizedImageUrl" :alt="`Resized Preview (${imageWidth}px x ${imageHeight}px)`" :style="{ width: `${imageWidth}px`, height: `${imageHeight}px` }">
131+
</div>
132+
<p>Preview: {{ imageWidth }}x{{ imageHeight }}px</p>
133+
134+
<!-- Download options -->
135+
<h3>Download Options:</h3>
136+
<div class="download-grid">
137+
<n-button @click.prevent="downloadImage('jpg')">
138+
Download JPG
139+
</n-button>
140+
<n-button @click.prevent="downloadImage('png')">
141+
Download PNG
142+
</n-button>
143+
<n-button @click.prevent="downloadImage('bmp')">
144+
Download BMP
145+
</n-button>
146+
<n-button @click.prevent="downloadImage('svg')">
147+
Download SVG
148+
</n-button>
149+
<n-button @click.prevent="downloadImage('ico')">
150+
Download ICO
151+
</n-button>
152+
</div>
153+
</div>
154+
</n-card>
155+
</template>
156+
157+
<style scoped>
158+
.input-group {
159+
display: flex;
160+
flex-direction: column;
161+
justify-content: center;
162+
align-items: left;
163+
}
164+
165+
.input-group label {
166+
margin-right: 10px;
167+
width: 100px;
168+
}
169+
170+
.image-container {
171+
max-width: 100%;
172+
overflow: auto;
173+
}
174+
175+
.image-wrapper {
176+
max-width: 100%;
177+
max-height: 500px;
178+
overflow: auto;
179+
}
180+
181+
.download-grid {
182+
display: flex;
183+
justify-content: center;
184+
flex-wrap: wrap;
185+
gap: 10px;
186+
margin-top: 10px;
187+
}
188+
189+
a {
190+
color: #007bff;
191+
text-decoration: none;
192+
}
193+
194+
a:hover {
195+
text-decoration: underline;
196+
}
197+
</style>

src/tools/image-resizer/index.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { IconResize } from '@tabler/icons-vue';
2+
import { defineTool } from '../tool';
3+
4+
export const tool = defineTool({
5+
name: 'Image resizer',
6+
path: '/image-resizer',
7+
description: '',
8+
keywords: ['image', 'resizer', 'favicon', 'jpg', 'jpeg', 'png', 'bmp', 'ico', 'svg'],
9+
component: () => import('./image-resizer.vue'),
10+
icon: IconResize,
11+
createdAt: new Date('2024-10-22'),
12+
});

src/tools/index.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { tool as base64FileConverter } from './base64-file-converter';
22
import { tool as base64StringConverter } from './base64-string-converter';
33
import { tool as basicAuthGenerator } from './basic-auth-generator';
4+
import { tool as imageResizer } from './image-resizer';
45
import { tool as emailNormalizer } from './email-normalizer';
56

67
import { tool as asciiTextDrawer } from './ascii-text-drawer';
@@ -141,7 +142,13 @@ export const toolsByCategory: ToolCategory[] = [
141142
},
142143
{
143144
name: 'Images and videos',
144-
components: [qrCodeGenerator, wifiQrCodeGenerator, svgPlaceholderGenerator, cameraRecorder],
145+
components: [
146+
qrCodeGenerator,
147+
wifiQrCodeGenerator,
148+
svgPlaceholderGenerator,
149+
cameraRecorder,
150+
imageResizer,
151+
],
145152
},
146153
{
147154
name: 'Development',

0 commit comments

Comments
 (0)