mirror of
https://github.com/CorentinTh/it-tools.git
synced 2024-12-26 01:31:30 +08:00
feat(new tool) image-resizer
This commit is contained in:
parent
0b1b98f93e
commit
04d3fa221c
5 changed files with 222 additions and 1 deletions
1
components.d.ts
vendored
1
components.d.ts
vendored
|
@ -101,6 +101,7 @@ declare module '@vue/runtime-core' {
|
||||||
IconMdiSearch: typeof import('~icons/mdi/search')['default']
|
IconMdiSearch: typeof import('~icons/mdi/search')['default']
|
||||||
IconMdiTranslate: typeof import('~icons/mdi/translate')['default']
|
IconMdiTranslate: typeof import('~icons/mdi/translate')['default']
|
||||||
IconMdiTriangleDown: typeof import('~icons/mdi/triangle-down')['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']
|
InputCopyable: typeof import('./src/components/InputCopyable.vue')['default']
|
||||||
IntegerBaseConverter: typeof import('./src/tools/integer-base-converter/integer-base-converter.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']
|
Ipv4AddressConverter: typeof import('./src/tools/ipv4-address-converter/ipv4-address-converter.vue')['default']
|
||||||
|
|
|
@ -392,3 +392,7 @@ tools:
|
||||||
text-to-binary:
|
text-to-binary:
|
||||||
title: Text to ASCII binary
|
title: Text to ASCII binary
|
||||||
description: Convert text to its ASCII binary representation and vice-versa.
|
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)
|
197
src/tools/image-resizer/image-resizer.vue
Normal file
197
src/tools/image-resizer/image-resizer.vue
Normal file
|
@ -0,0 +1,197 @@
|
||||||
|
<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
|
||||||
|
async function handleFileUpload(uploadedFile: File) {
|
||||||
|
if (!uploadedFile) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
imageFile.value = uploadedFile;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Read the file as a Data URL
|
||||||
|
const reader = new FileReader();
|
||||||
|
const fileDataUrl = await new Promise<string>((resolve, reject) => {
|
||||||
|
reader.onload = () => resolve(reader.result as string);
|
||||||
|
reader.onerror = error => reject(error);
|
||||||
|
reader.readAsDataURL(uploadedFile);
|
||||||
|
});
|
||||||
|
|
||||||
|
imageUrl.value = fileDataUrl; // Preview image
|
||||||
|
originalImageUrl.value = fileDataUrl; // 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 = fileDataUrl;
|
||||||
|
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error('Error reading file:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 -->
|
||||||
|
<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" />
|
||||||
|
|
||||||
|
<!-- 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
src/tools/image-resizer/index.ts
Normal file
12
src/tools/image-resizer/index.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import { IconResize } from '@tabler/icons-vue';
|
||||||
|
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: IconResize,
|
||||||
|
createdAt: new Date('2024-10-22'),
|
||||||
|
});
|
|
@ -1,6 +1,7 @@
|
||||||
import { tool as base64FileConverter } from './base64-file-converter';
|
import { tool as base64FileConverter } from './base64-file-converter';
|
||||||
import { tool as base64StringConverter } from './base64-string-converter';
|
import { tool as base64StringConverter } from './base64-string-converter';
|
||||||
import { tool as basicAuthGenerator } from './basic-auth-generator';
|
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 emailNormalizer } from './email-normalizer';
|
||||||
|
|
||||||
import { tool as asciiTextDrawer } from './ascii-text-drawer';
|
import { tool as asciiTextDrawer } from './ascii-text-drawer';
|
||||||
|
@ -141,7 +142,13 @@ export const toolsByCategory: ToolCategory[] = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Images and videos',
|
name: 'Images and videos',
|
||||||
components: [qrCodeGenerator, wifiQrCodeGenerator, svgPlaceholderGenerator, cameraRecorder],
|
components: [
|
||||||
|
qrCodeGenerator,
|
||||||
|
wifiQrCodeGenerator,
|
||||||
|
svgPlaceholderGenerator,
|
||||||
|
cameraRecorder,
|
||||||
|
imageResizer,
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Development',
|
name: 'Development',
|
||||||
|
|
Loading…
Reference in a new issue