Skip to content

Commit 4bf7470

Browse files
committed
feat(new tool): ICO <> PNG Converter
Fix CorentinTh#1271
1 parent 80e46c9 commit 4bf7470

File tree

8 files changed

+276
-61
lines changed

8 files changed

+276
-61
lines changed

components.d.ts

Lines changed: 2 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -87,30 +87,19 @@ declare module '@vue/runtime-core' {
8787
HtmlWysiwygEditor: typeof import('./src/tools/html-wysiwyg-editor/html-wysiwyg-editor.vue')['default']
8888
HttpStatusCodes: typeof import('./src/tools/http-status-codes/http-status-codes.vue')['default']
8989
IbanValidatorAndParser: typeof import('./src/tools/iban-validator-and-parser/iban-validator-and-parser.vue')['default']
90+
IcoConverter: typeof import('./src/tools/ico-converter/ico-converter.vue')['default']
9091
'IconMdi:brushVariant': typeof import('~icons/mdi/brush-variant')['default']
91-
'IconMdi:contentCopy': typeof import('~icons/mdi/content-copy')['default']
9292
'IconMdi:kettleSteamOutline': typeof import('~icons/mdi/kettle-steam-outline')['default']
93-
IconMdiArrowDown: typeof import('~icons/mdi/arrow-down')['default']
94-
IconMdiArrowRight: typeof import('~icons/mdi/arrow-right')['default']
95-
IconMdiArrowRightBottom: typeof import('~icons/mdi/arrow-right-bottom')['default']
96-
IconMdiCamera: typeof import('~icons/mdi/camera')['default']
9793
IconMdiChevronDown: typeof import('~icons/mdi/chevron-down')['default']
9894
IconMdiChevronRight: typeof import('~icons/mdi/chevron-right')['default']
9995
IconMdiClose: typeof import('~icons/mdi/close')['default']
10096
IconMdiContentCopy: typeof import('~icons/mdi/content-copy')['default']
101-
IconMdiDeleteOutline: typeof import('~icons/mdi/delete-outline')['default']
102-
IconMdiDownload: typeof import('~icons/mdi/download')['default']
10397
IconMdiEye: typeof import('~icons/mdi/eye')['default']
10498
IconMdiEyeOff: typeof import('~icons/mdi/eye-off')['default']
10599
IconMdiHeart: typeof import('~icons/mdi/heart')['default']
106-
IconMdiPause: typeof import('~icons/mdi/pause')['default']
107-
IconMdiPlay: typeof import('~icons/mdi/play')['default']
108-
IconMdiRecord: typeof import('~icons/mdi/record')['default']
109-
IconMdiRefresh: typeof import('~icons/mdi/refresh')['default']
110100
IconMdiSearch: typeof import('~icons/mdi/search')['default']
111101
IconMdiTranslate: typeof import('~icons/mdi/translate')['default']
112102
IconMdiTriangleDown: typeof import('~icons/mdi/triangle-down')['default']
113-
IconMdiVideo: typeof import('~icons/mdi/video')['default']
114103
InputCopyable: typeof import('./src/components/InputCopyable.vue')['default']
115104
IntegerBaseConverter: typeof import('./src/tools/integer-base-converter/integer-base-converter.vue')['default']
116105
Ipv4AddressConverter: typeof import('./src/tools/ipv4-address-converter/ipv4-address-converter.vue')['default']
@@ -137,42 +126,22 @@ declare module '@vue/runtime-core' {
137126
MenuLayout: typeof import('./src/components/MenuLayout.vue')['default']
138127
MetaTagGenerator: typeof import('./src/tools/meta-tag-generator/meta-tag-generator.vue')['default']
139128
MimeTypes: typeof import('./src/tools/mime-types/mime-types.vue')['default']
140-
NAlert: typeof import('naive-ui')['NAlert']
141129
NavbarButtons: typeof import('./src/components/NavbarButtons.vue')['default']
142-
NCheckbox: typeof import('naive-ui')['NCheckbox']
143-
NCode: typeof import('naive-ui')['NCode']
144130
NCollapseTransition: typeof import('naive-ui')['NCollapseTransition']
145-
NColorPicker: typeof import('naive-ui')['NColorPicker']
146131
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
147-
NDatePicker: typeof import('naive-ui')['NDatePicker']
148132
NDivider: typeof import('naive-ui')['NDivider']
149-
NDynamicInput: typeof import('naive-ui')['NDynamicInput']
150133
NEllipsis: typeof import('naive-ui')['NEllipsis']
151-
NForm: typeof import('naive-ui')['NForm']
152-
NFormItem: typeof import('naive-ui')['NFormItem']
153134
NGi: typeof import('naive-ui')['NGi']
154135
NGrid: typeof import('naive-ui')['NGrid']
155136
NH1: typeof import('naive-ui')['NH1']
156-
NH2: typeof import('naive-ui')['NH2']
157137
NH3: typeof import('naive-ui')['NH3']
158138
NIcon: typeof import('naive-ui')['NIcon']
159-
NImage: typeof import('naive-ui')['NImage']
160-
NInputGroup: typeof import('naive-ui')['NInputGroup']
161-
NInputGroupLabel: typeof import('naive-ui')['NInputGroupLabel']
162-
NInputNumber: typeof import('naive-ui')['NInputNumber']
163139
NLayout: typeof import('naive-ui')['NLayout']
164140
NLayoutSider: typeof import('naive-ui')['NLayoutSider']
165141
NMenu: typeof import('naive-ui')['NMenu']
166-
NProgress: typeof import('naive-ui')['NProgress']
167-
NScrollbar: typeof import('naive-ui')['NScrollbar']
168-
NSlider: typeof import('naive-ui')['NSlider']
169-
NStatistic: typeof import('naive-ui')['NStatistic']
170-
NSwitch: typeof import('naive-ui')['NSwitch']
171-
NTable: typeof import('naive-ui')['NTable']
142+
NSpin: typeof import('naive-ui')['NSpin']
172143
NTag: typeof import('naive-ui')['NTag']
173144
NumeronymGenerator: typeof import('./src/tools/numeronym-generator/numeronym-generator.vue')['default']
174-
NUpload: typeof import('naive-ui')['NUpload']
175-
NUploadDragger: typeof import('naive-ui')['NUploadDragger']
176145
OtpCodeGeneratorAndValidator: typeof import('./src/tools/otp-code-generator-and-validator/otp-code-generator-and-validator.vue')['default']
177146
PasswordStrengthAnalyser: typeof import('./src/tools/password-strength-analyser/password-strength-analyser.vue')['default']
178147
PdfSignatureChecker: typeof import('./src/tools/pdf-signature-checker/pdf-signature-checker.vue')['default']

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@
6262
"highlight.js": "^11.7.0",
6363
"iarna-toml-esm": "^3.0.5",
6464
"ibantools": "^4.3.3",
65+
"image-in-browser": "^3.1.0",
66+
"js-base64": "^3.7.7",
6567
"json5": "^2.2.3",
6668
"jwt-decode": "^3.1.2",
6769
"libphonenumber-js": "^1.10.28",

pnpm-lock.yaml

Lines changed: 23 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/composable/downloadBase64.ts

Lines changed: 71 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
1-
import { extension as getExtensionFromMime } from 'mime-types';
2-
import type { Ref } from 'vue';
1+
import { extension as getExtensionFromMimeType, extension as getMimeTypeFromExtension } from 'mime-types';
2+
import type { MaybeRef, Ref } from 'vue';
33
import _ from 'lodash';
4+
import { get } from '@vueuse/core';
45

5-
export { getMimeTypeFromBase64, useDownloadFileFromBase64 };
6+
export {
7+
getMimeTypeFromBase64,
8+
getMimeTypeFromExtension, getExtensionFromMimeType,
9+
useDownloadFileFromBase64, useDownloadFileFromBase64Refs,
10+
previewImageFromBase64,
11+
};
612

713
const commonMimeTypesSignatures = {
814
'JVBERi0': 'application/pdf',
@@ -36,30 +42,78 @@ function getFileExtensionFromMimeType({
3642
defaultExtension?: string
3743
}) {
3844
if (mimeType) {
39-
return getExtensionFromMime(mimeType) ?? defaultExtension;
45+
return getExtensionFromMimeType(mimeType) ?? defaultExtension;
4046
}
4147

4248
return defaultExtension;
4349
}
4450

45-
function useDownloadFileFromBase64({ source, filename }: { source: Ref<string>; filename?: string }) {
51+
function downloadFromBase64({ sourceValue, filename, extension, fileMimeType }:
52+
{ sourceValue: string; filename?: string; extension?: string; fileMimeType?: string }) {
53+
if (sourceValue === '') {
54+
throw new Error('Base64 string is empty');
55+
}
56+
57+
const defaultExtension = extension ?? 'txt';
58+
const { mimeType } = getMimeTypeFromBase64({ base64String: sourceValue });
59+
let base64String = sourceValue;
60+
if (!mimeType) {
61+
const targetMimeType = fileMimeType ?? getMimeTypeFromExtension(defaultExtension);
62+
base64String = `data:${targetMimeType};base64,${sourceValue}`;
63+
}
64+
65+
const cleanExtension = extension ?? getFileExtensionFromMimeType(
66+
{ mimeType, defaultExtension });
67+
let cleanFileName = filename ?? `file.${cleanExtension}`;
68+
if (extension && !cleanFileName.endsWith(`.${extension}`)) {
69+
cleanFileName = `${cleanFileName}.${cleanExtension}`;
70+
}
71+
72+
const a = document.createElement('a');
73+
a.href = base64String;
74+
a.download = cleanFileName;
75+
a.click();
76+
}
77+
78+
function useDownloadFileFromBase64(
79+
{ source, filename, extension }:
80+
{ source: MaybeRef<string>; filename?: MaybeRef<string>; extension?: MaybeRef<string> }) {
4681
return {
4782
download() {
48-
if (source.value === '') {
49-
throw new Error('Base64 string is empty');
50-
}
83+
downloadFromBase64({ sourceValue: get(source), filename: get(filename), extension: get(extension) });
84+
},
85+
};
86+
}
87+
88+
function previewImageFromBase64(base64String: string): HTMLImageElement {
89+
if (base64String === '') {
90+
throw new Error('Base64 string is empty');
91+
}
92+
93+
const img = document.createElement('img');
94+
img.src = base64String;
95+
96+
const container = document.createElement('div');
97+
container.appendChild(img);
5198

52-
const { mimeType } = getMimeTypeFromBase64({ base64String: source.value });
53-
const base64String = mimeType
54-
? source.value
55-
: `data:text/plain;base64,${source.value}`;
99+
const previewContainer = document.getElementById('previewContainer');
100+
if (previewContainer) {
101+
previewContainer.innerHTML = '';
102+
previewContainer.appendChild(container);
103+
}
104+
else {
105+
throw new Error('Preview container element not found');
106+
}
56107

57-
const cleanFileName = filename ?? `file.${getFileExtensionFromMimeType({ mimeType })}`;
108+
return img;
109+
}
58110

59-
const a = document.createElement('a');
60-
a.href = base64String;
61-
a.download = cleanFileName;
62-
a.click();
111+
function useDownloadFileFromBase64Refs(
112+
{ source, filename, extension }:
113+
{ source: Ref<string>; filename?: Ref<string>; extension?: Ref<string> }) {
114+
return {
115+
download() {
116+
downloadFromBase64({ sourceValue: source.value, filename: filename?.value, extension: extension?.value });
63117
},
64118
};
65119
}
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+
});

0 commit comments

Comments
 (0)