Skip to content

Commit 8361798

Browse files
committed
Merge branch 'feat/ico-png-converter' into chore/all-my-stuffs
# Conflicts: # components.d.ts # pnpm-lock.yaml # src/tools/index.ts
2 parents fccc9db + 2c9168e commit 8361798

File tree

6 files changed

+182
-3
lines changed

6 files changed

+182
-3
lines changed

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,8 @@
120120
"ical-generator": "^8.0.0",
121121
"ical.js": "^2.1.0",
122122
"js-base64": "^3.7.6",
123+
"image-in-browser": "^3.1.0",
124+
"js-base64": "^3.7.7",
123125
"json5": "^2.2.3",
124126
"jwt-decode": "^3.1.2",
125127
"libphonenumber-js": "^1.10.28",

pnpm-lock.yaml

Lines changed: 8 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
<script setup lang="ts">
2+
import { Base64 } from 'js-base64';
3+
import { Transform, decodeIco, decodeImage, encodeIcoImages, encodePng } from 'image-in-browser';
4+
import { useDownloadFileFromBase64 } from '@/composable/downloadBase64';
5+
6+
const status = ref<'idle' | 'done' | 'error' | 'processing'>('idle');
7+
const file = ref<File | null>(null);
8+
9+
const base64OutputFile = ref('');
10+
const fileName = ref('');
11+
const fileExtension = ref('');
12+
const { download } = useDownloadFileFromBase64(
13+
{
14+
source: base64OutputFile,
15+
filename: fileName,
16+
extension: fileExtension,
17+
});
18+
19+
async function onFileUploaded(uploadedFile: File) {
20+
file.value = uploadedFile;
21+
const fileBuffer = new Uint8Array(await uploadedFile.arrayBuffer());
22+
23+
fileName.value = `${uploadedFile.name}`;
24+
status.value = 'processing';
25+
try {
26+
if (uploadedFile.type.includes('icon')) {
27+
const decodedIco = decodeIco({
28+
data: fileBuffer,
29+
largest: true,
30+
});
31+
if (decodedIco == null) {
32+
throw new Error('Invalid ICO file!');
33+
}
34+
const encodedPng = encodePng({
35+
image: decodedIco,
36+
});
37+
fileExtension.value = 'png';
38+
base64OutputFile.value = `data:image/png;base64,${Base64.fromUint8Array(encodedPng)}`;
39+
}
40+
else {
41+
const decodedImage = decodeImage({
42+
data: fileBuffer,
43+
});
44+
45+
if (decodedImage == null) {
46+
throw new Error('Invalid PNG file!');
47+
};
48+
49+
const encodedICO = encodeIcoImages({
50+
images: [16, 32, 64, 128, 256].map(size => Transform.copyResize({
51+
image: decodedImage,
52+
width: size,
53+
maintainAspect: true,
54+
})),
55+
});
56+
fileExtension.value = 'ico';
57+
base64OutputFile.value = `data:image/x-icon;base64,${Base64.fromUint8Array(encodedICO)}`;
58+
}
59+
status.value = 'done';
60+
61+
download();
62+
}
63+
catch (e) {
64+
status.value = 'error';
65+
}
66+
}
67+
</script>
68+
69+
<template>
70+
<div>
71+
<div style="flex: 0 0 100%">
72+
<div mx-auto max-w-600px>
73+
<c-file-upload
74+
title="Drag and drop an ICO or PNG/JPEG file here, or click to select a file"
75+
accept=".ico,.png,.jpg"
76+
paste-image
77+
@file-upload="onFileUploaded"
78+
/>
79+
</div>
80+
</div>
81+
82+
<div mt-3 flex justify-center>
83+
<c-alert v-if="status === 'error'" type="error">
84+
An error occured processing {{ fileName }}
85+
</c-alert>
86+
<n-spin
87+
v-if="status === 'processing'"
88+
size="small"
89+
/>
90+
</div>
91+
</div>
92+
</template>

src/tools/ico-converter/index.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { PictureInPicture } from '@vicons/tabler';
2+
import { defineTool } from '../tool';
3+
4+
export const tool = defineTool({
5+
name: 'ICO/PNG converter',
6+
path: '/ico-converter',
7+
description: 'Convert from PNG/JPEG to/from ICO',
8+
keywords: ['ico', 'png', 'jpeg', 'icon', 'converter'],
9+
component: () => import('./ico-converter.vue'),
10+
icon: PictureInPicture,
11+
createdAt: new Date('2024-08-15'),
12+
});

src/tools/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ 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';
44
import { tool as dnsQueries } from './dns-queries';
5+
import { tool as icoConverter } from './ico-converter';
56
import { tool as emailNormalizer } from './email-normalizer';
67
import { tool as bounceParser } from './bounce-parser';
78
import { tool as codeHighlighter } from './code-highlighter';
@@ -205,6 +206,7 @@ export const toolsByCategory: ToolCategory[] = [
205206
barcodeReader,
206207
barcodeGenerator,
207208
heicConverter,
209+
icoConverter,
208210
],
209211
},
210212
{

src/ui/c-file-upload/c-file-upload.vue

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,44 @@ const props = withDefaults(defineProps<{
55
multiple?: boolean
66
accept?: string
77
title?: string
8+
pasteImage?: boolean
89
}>(), {
910
multiple: false,
1011
accept: undefined,
1112
title: 'Drag and drop files here, or click to select files',
13+
pasteImage: false,
1214
});
1315
1416
const emit = defineEmits<{
1517
(event: 'filesUpload', files: File[]): void
1618
(event: 'fileUpload', file: File): void
1719
}>();
1820
19-
const { multiple } = toRefs(props);
21+
const { multiple, pasteImage } = toRefs(props);
2022
2123
const isOverDropZone = ref(false);
2224
25+
function toBase64(file: File) {
26+
return new Promise<string>((resolve, reject) => {
27+
const reader = new FileReader();
28+
reader.readAsDataURL(file);
29+
reader.onload = () => resolve(reader.result?.toString() ?? '');
30+
reader.onerror = error => reject(error);
31+
});
32+
}
33+
2334
const fileInput = ref<HTMLInputElement | null>(null);
35+
const imgPreview = ref<HTMLImageElement | null>(null);
36+
async function handlePreview(image: File) {
37+
if (imgPreview.value) {
38+
imgPreview.value.src = await toBase64(image);
39+
}
40+
}
41+
function clearPreview() {
42+
if (imgPreview.value) {
43+
imgPreview.value.src = '';
44+
}
45+
}
2446
2547
function triggerFileInput() {
2648
fileInput.value?.click();
@@ -39,7 +61,30 @@ function handleDrop(event: DragEvent) {
3961
handleUpload(files);
4062
}
4163
42-
function handleUpload(files: FileList | null | undefined) {
64+
async function onPasteImage(evt: ClipboardEvent) {
65+
if (!pasteImage.value) {
66+
return false;
67+
}
68+
69+
const items = evt.clipboardData?.items;
70+
if (!items) {
71+
return false;
72+
}
73+
for (let i = 0; i < items.length; i++) {
74+
if (items[i].type.includes('image')) {
75+
const imageFile = items[i].getAsFile();
76+
if (imageFile) {
77+
await handlePreview(imageFile);
78+
emit('fileUpload', imageFile);
79+
}
80+
}
81+
}
82+
return true;
83+
}
84+
85+
async function handleUpload(files: FileList | null | undefined) {
86+
clearPreview();
87+
4388
if (_.isNil(files) || _.isEmpty(files)) {
4489
return;
4590
}
@@ -49,6 +94,7 @@ function handleUpload(files: FileList | null | undefined) {
4994
return;
5095
}
5196
97+
await handlePreview(files[0]);
5298
emit('fileUpload', files[0]);
5399
}
54100
</script>
@@ -60,6 +106,7 @@ function handleUpload(files: FileList | null | undefined) {
60106
'border-primary border-opacity-100': isOverDropZone,
61107
}"
62108
@click="triggerFileInput"
109+
@paste.prevent="onPasteImage"
63110
@drop.prevent="handleDrop"
64111
@dragover.prevent
65112
@dragenter="isOverDropZone = true"
@@ -73,6 +120,7 @@ function handleUpload(files: FileList | null | undefined) {
73120
:accept="accept"
74121
@change="handleFileInput"
75122
>
123+
76124
<slot>
77125
<span op-70>
78126
{{ title }}
@@ -90,6 +138,22 @@ function handleUpload(files: FileList | null | undefined) {
90138
<c-button>
91139
Browse files
92140
</c-button>
141+
142+
<div v-if="pasteImage">
143+
<!-- separator -->
144+
<div my-4 w-full flex items-center justify-center op-70>
145+
<div class="h-1px max-w-100px flex-1 bg-gray-300 op-50" />
146+
<div class="mx-2 text-gray-400">
147+
or
148+
</div>
149+
<div class="h-1px max-w-100px flex-1 bg-gray-300 op-50" />
150+
</div>
151+
152+
<p>Paste an image from clipboard</p>
153+
</div>
93154
</slot>
155+
<div mt-2>
156+
<img ref="imgPreview" width="150">
157+
</div>
94158
</div>
95159
</template>

0 commit comments

Comments
 (0)