Skip to content

Commit

Permalink
feat(new tool) image-resizer
Browse files Browse the repository at this point in the history
  • Loading branch information
gitmotion committed Oct 23, 2024
1 parent 5732483 commit 3823c12
Show file tree
Hide file tree
Showing 5 changed files with 207 additions and 1 deletion.
1 change: 1 addition & 0 deletions components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ declare module '@vue/runtime-core' {
IconMdiSearch: typeof import('~icons/mdi/search')['default']
IconMdiTranslate: typeof import('~icons/mdi/translate')['default']
IconMdiTriangleDown: typeof import('~icons/mdi/triangle-down')['default']
ImageResizer: typeof import('./src/tools/image-resizer/image-resizer.vue')['default']
InputCopyable: typeof import('./src/components/InputCopyable.vue')['default']
IntegerBaseConverter: typeof import('./src/tools/integer-base-converter/integer-base-converter.vue')['default']
Ipv4AddressConverter: typeof import('./src/tools/ipv4-address-converter/ipv4-address-converter.vue')['default']
Expand Down
4 changes: 4 additions & 0 deletions locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -391,3 +391,7 @@ tools:
text-to-binary:
title: Text to ASCII binary
description: Convert text to its ASCII binary representation and vice-versa.

image-resizer:
title: Image resizer
description: Convert the width and height of an image file, preview, and download it in a desired format (.jpg, .jpeg, .png, .bmp, .ico, .svg)
182 changes: 182 additions & 0 deletions src/tools/image-resizer/image-resizer.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
<script setup lang="ts">
import { ref, watch } from 'vue';
// State variables
const imageFile = ref<File | null>(null);
const imageUrl = ref<string | null>(null);
const originalImageUrl = ref<string | null>(null); // To store original image data
const imageWidth = ref(500); // Default width
const imageHeight = ref(350); // Default height
const originalImageWidth = ref<number | null>(null); // Store original image width
const originalImageHeight = ref<number | null>(null); // Store original image height
const resizedImageUrl = ref<string | null>(null);
// Watch width to trigger resizing
watch(imageWidth, () => {
resizeImage();
});
// Watch height to trigger resizing
watch(imageHeight, () => {
resizeImage();
});
// Handle file upload
function handleFileUpload(event: Event) {
const fileInput = event.target as HTMLInputElement;
const file = fileInput.files?.[0];
if (file) {
imageFile.value = file;
const reader = new FileReader();
reader.onload = (e) => {
imageUrl.value = e.target?.result as string; // Preview image
originalImageUrl.value = imageUrl.value; // Store original image for resizing
resizedImageUrl.value = null; // Clear previous resized image
// Create an image to get original dimensions
const img = new Image();
img.src = imageUrl.value;
img.onload = () => {
// Set original image dimensions
originalImageWidth.value = img.naturalWidth;
originalImageHeight.value = img.naturalHeight;
// Automatically resize if width and height are set
if (imageWidth.value > 0 && imageHeight.value > 0) {
resizeImage();
}
};
};
reader.readAsDataURL(file);
}
}
// Function to resize the image
async function resizeImage() {
if (!originalImageUrl.value) {
return; // Ensure there's an original image to work with
}
const img = new Image();
img.src = originalImageUrl.value; // Use the original image for resizing
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = imageWidth.value;
canvas.height = imageHeight.value;
const ctx = canvas.getContext('2d');
ctx?.drawImage(img, 0, 0, imageWidth.value, imageHeight.value);
resizedImageUrl.value = canvas.toDataURL('image/png');
};
}
// Function to download resized image
function downloadImage(format: string) {
if (!resizedImageUrl.value || !imageFile.value) {
return;
}
const originalFilename = imageFile.value.name.replace(/\.[^/.]+$/, ''); // Remove file extension
const newFilename = `${originalFilename}-${imageWidth.value}x${imageHeight.value}.${format}`;
const link = document.createElement('a');
link.href = resizedImageUrl.value;
link.download = newFilename;
link.click();
}
</script>

<template>
<n-card>
<div>
<!-- File input -->
<input type="file" mb-2 accept=".jpg,.jpeg,.png,.bmp,.ico,.svg" @change="handleFileUpload">

<!-- Original image dimensions -->
<div v-if="originalImageWidth && originalImageHeight">
<p>Original Image Dimensions: {{ originalImageWidth }}x{{ originalImageHeight }}px</p>
</div>

<!-- Width and height inputs -->
<div class="input-group">
<label for="widthInput">Width (px):</label>
<n-input-number id="widthInput" v-model:value="imageWidth" placeholder="Width (px)" mb-3 />
</div>

<div class="input-group">
<label for="heightInput">Height (px):</label>
<n-input-number id="heightInput" v-model:value="imageHeight" placeholder="Height (px)" mb-1 />
</div>
</div>

<!-- Image preview -->
<div v-if="resizedImageUrl" class="image-container" style="text-align: center; margin-top: 20px;">
<div class="image-wrapper">
<img :src="resizedImageUrl" :alt="`Resized Preview (${imageWidth}px x ${imageHeight}px)`" :style="{ width: `${imageWidth}px`, height: `${imageHeight}px` }">
</div>
<p>Preview: {{ imageWidth }}x{{ imageHeight }}px</p>

<!-- Download options -->
<h3>Download Options:</h3>
<div class="download-grid">
<n-button @click.prevent="downloadImage('jpg')">
Download JPG
</n-button>
<n-button @click.prevent="downloadImage('png')">
Download PNG
</n-button>
<n-button @click.prevent="downloadImage('bmp')">
Download BMP
</n-button>
<n-button @click.prevent="downloadImage('svg')">
Download SVG
</n-button>
<n-button @click.prevent="downloadImage('ico')">
Download ICO
</n-button>
</div>
</div>
</n-card>
</template>

<style scoped>
.input-group {
display: flex;
flex-direction: column;
justify-content: center;
align-items: left;
}
.input-group label {
margin-right: 10px;
width: 100px;
}
.image-container {
max-width: 100%;
overflow: auto;
}
.image-wrapper {
max-width: 100%;
max-height: 500px;
overflow: auto;
}
.download-grid {
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 10px;
margin-top: 10px;
}
a {
color: #007bff;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
</style>
12 changes: 12 additions & 0 deletions src/tools/image-resizer/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Resize } from '@vicons/tabler';
import { defineTool } from '../tool';

export const tool = defineTool({
name: 'Image resizer',
path: '/image-resizer',
description: '',
keywords: ['image', 'resizer', 'favicon', 'jpg', 'jpeg', 'png', 'bmp', 'ico', 'svg'],
component: () => import('./image-resizer.vue'),
icon: Resize,
createdAt: new Date('2024-10-22'),
});
9 changes: 8 additions & 1 deletion src/tools/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { tool as base64FileConverter } from './base64-file-converter';
import { tool as base64StringConverter } from './base64-string-converter';
import { tool as basicAuthGenerator } from './basic-auth-generator';
import { tool as imageResizer } from './image-resizer';
import { tool as emailNormalizer } from './email-normalizer';

import { tool as asciiTextDrawer } from './ascii-text-drawer';
Expand Down Expand Up @@ -141,7 +142,13 @@ export const toolsByCategory: ToolCategory[] = [
},
{
name: 'Images and videos',
components: [qrCodeGenerator, wifiQrCodeGenerator, svgPlaceholderGenerator, cameraRecorder],
components: [
qrCodeGenerator,
wifiQrCodeGenerator,
svgPlaceholderGenerator,
cameraRecorder,
imageResizer,
],
},
{
name: 'Development',
Expand Down

0 comments on commit 3823c12

Please sign in to comment.