Skip to content

Commit d3eca6f

Browse files
committed
Merge branch 'feat/image-converter' into chore/all-my-stuffs
# Conflicts: # package.json # pnpm-lock.yaml # src/tools/index.ts
2 parents 8361798 + e9a5541 commit d3eca6f

File tree

7 files changed

+215
-0
lines changed

7 files changed

+215
-0
lines changed

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@
119119
"js-base64": "^3.7.7",
120120
"ical-generator": "^8.0.0",
121121
"ical.js": "^2.1.0",
122+
"image-in-browser": "^3.2.0",
122123
"js-base64": "^3.7.6",
123124
"image-in-browser": "^3.1.0",
124125
"js-base64": "^3.7.7",
@@ -155,6 +156,9 @@
155156
"sql-formatter": "^13.0.0",
156157
"sshpk": "^1.18.0",
157158
"turndown": "^7.1.2",
159+
"roboto-base64": "^0.1.2",
160+
"sql-formatter": "^13.0.0",
161+
"svg2png-wasm": "^1.4.1",
158162
"ua-parser-js": "^1.0.35",
159163
"ulid": "^2.3.0",
160164
"unicode-emoji-json": "^0.4.0",
@@ -166,6 +170,7 @@
166170
"vue-router": "^4.1.6",
167171
"vue-shadow-dom": "^4.2.0",
168172
"vue-tsc": "^1.8.1",
173+
"webp-converter-browser": "^1.0.4",
169174
"vuedraggable": "^4.1.0",
170175
"xml-formatter": "^3.3.2",
171176
"xml-js": "^1.6.11",

pnpm-lock.yaml

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

public/svg2png_wasm_bg.wasm

1.9 MB
Binary file not shown.
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
<script setup lang="ts">
2+
import { Base64 } from 'js-base64';
3+
import type { MemoryImage } from 'image-in-browser';
4+
import { decodeImage, encodeBmp, encodeGif, encodeIco, encodeJpg, encodePng, encodePvr, encodeTga, encodeTiff } from 'image-in-browser';
5+
import { arrayBufferToWebP } from 'webp-converter-browser';
6+
import { createSvg2png, initialize } from 'svg2png-wasm';
7+
import { normal as robotoBase64 } from 'roboto-base64';
8+
import { useDownloadFileFromBase64 } from '@/composable/downloadBase64';
9+
import { useQueryParamOrStorage } from '@/composable/queryParams';
10+
11+
function readAsText(file: File) {
12+
return new Promise<string>((resolve, reject) => {
13+
const reader = new FileReader();
14+
reader.readAsText(file);
15+
reader.onload = () => resolve(reader.result?.toString() ?? '');
16+
reader.onerror = error => reject(error);
17+
});
18+
}
19+
20+
const status = ref<'idle' | 'done' | 'error' | 'processing'>('idle');
21+
const file = ref<File | null>(null);
22+
23+
const svgScale = ref(2);
24+
const base64OutputFile = ref('');
25+
const fileName = ref('');
26+
const fileExtension = ref('');
27+
const { download } = useDownloadFileFromBase64(
28+
{
29+
source: base64OutputFile,
30+
filename: fileName,
31+
extension: fileExtension,
32+
});
33+
34+
const outputQuality = useQueryParamOrStorage({ name: 'qual', storageName: 'imgconv:q', defaultValue: 0.95 });
35+
const outputFormats = {
36+
png: {
37+
mime: 'image/png',
38+
save: (image: MemoryImage) => encodePng({ image }),
39+
},
40+
jpg: {
41+
mime: 'image/jpeg',
42+
save: (image: MemoryImage) => encodeJpg({ image, quality: outputQuality.value }),
43+
},
44+
bmp: {
45+
mime: 'image/bmp',
46+
save: (image: MemoryImage) => encodeBmp({ image }),
47+
},
48+
gif: {
49+
mime: 'image/gif',
50+
save: (image: MemoryImage) => encodeGif({ image }),
51+
},
52+
ico: {
53+
mime: 'image/x-icon',
54+
save: (image: MemoryImage) => encodeIco({ image }),
55+
},
56+
tga: {
57+
mime: 'image/tga',
58+
save: (image: MemoryImage) => encodeTga({ image }),
59+
},
60+
pvr: {
61+
mime: 'image/pvr',
62+
save: (image: MemoryImage) => encodePvr({ image }),
63+
},
64+
tif: {
65+
mime: 'image/tif',
66+
save: (image: MemoryImage) => encodeTiff({ image }),
67+
},
68+
webp: {
69+
mime: 'image/webp',
70+
save: () => null,
71+
},
72+
};
73+
74+
const outputFormat = useQueryParamOrStorage({ name: 'fmt', storageName: 'imgconv:fmt', defaultValue: 'png' });
75+
const outputFormatHasQuality = computed(() => {
76+
return outputFormat.value === 'jpg';
77+
});
78+
79+
const svgWasmLoaded = ref(false);
80+
81+
async function onFileUploaded(uploadedFile: File) {
82+
const outputFormatValue = outputFormat.value;
83+
file.value = uploadedFile;
84+
let fileBuffer = new Uint8Array(await uploadedFile.arrayBuffer());
85+
86+
fileName.value = `${uploadedFile.name}`;
87+
status.value = 'processing';
88+
try {
89+
if (outputFormatValue === 'webp') {
90+
const encodedImage = await arrayBufferToWebP(fileBuffer);
91+
fileExtension.value = 'webp';
92+
base64OutputFile.value = `data:image/webp;base64,${Base64.fromUint8Array(new Uint8Array(await encodedImage.arrayBuffer()))}`;
93+
}
94+
else {
95+
if (uploadedFile.type === 'image/svg+xml') {
96+
if (!svgWasmLoaded.value) {
97+
await initialize(fetch('/svg2png_wasm_bg.wasm'));
98+
svgWasmLoaded.value = true;
99+
}
100+
const svg2png = createSvg2png({
101+
fonts: [Base64.toUint8Array(robotoBase64)],
102+
});
103+
fileBuffer = await svg2png(await readAsText(uploadedFile), { scale: svgScale.value });
104+
svg2png.dispose();
105+
}
106+
const decodedImage = decodeImage({
107+
data: fileBuffer,
108+
});
109+
110+
if (decodedImage == null) {
111+
throw new Error('Invalid Image file!');
112+
};
113+
114+
const outConfig = outputFormats[outputFormatValue as (keyof typeof outputFormats)];
115+
const encodedImage = outConfig.save(decodedImage);
116+
fileExtension.value = outputFormatValue;
117+
base64OutputFile.value = `data:${outConfig.mime};base64,${Base64.fromUint8Array(encodedImage!)}`;
118+
}
119+
status.value = 'done';
120+
121+
download();
122+
}
123+
catch (e) {
124+
status.value = 'error';
125+
}
126+
}
127+
</script>
128+
129+
<template>
130+
<div>
131+
<div style="flex: 0 0 100%" mb-2>
132+
<div mx-auto max-w-600px>
133+
<c-file-upload
134+
title="Drag and drop an image file here, or click to select a file"
135+
accept="image/*"
136+
paste-image
137+
@file-upload="onFileUploaded"
138+
/>
139+
</div>
140+
</div>
141+
142+
<c-select
143+
v-model:value="outputFormat"
144+
label="Output format:"
145+
label-position="left"
146+
:options="Object.keys(outputFormats)"
147+
placeholder="Select output format"
148+
mb-2
149+
/>
150+
151+
<div mb-2 flex justify-center>
152+
<n-form-item v-if="outputFormatHasQuality" label="Output quality:" label-placement="left">
153+
<n-input-number v-model:value="outputQuality" :max="100" :min="0" w-full />
154+
</n-form-item>
155+
<n-form-item label="SVG scaling:" label-placement="left">
156+
<n-input-number v-model:value="svgScale" :min="0" />
157+
</n-form-item>
158+
</div>
159+
160+
<div mt-3 flex justify-center>
161+
<c-alert v-if="status === 'error'" type="error">
162+
An error occured processing {{ fileName }}
163+
</c-alert>
164+
<n-spin
165+
v-if="status === 'processing'"
166+
size="small"
167+
/>
168+
</div>
169+
</div>
170+
</template>

src/tools/image-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: 'Image Formats Converter',
6+
path: '/image-converter',
7+
description: 'Convert images from one format to another',
8+
keywords: ['image', 'bmp', 'gif', 'ico', 'jpg', 'png', 'tga', 'pvr', 'tiff', 'pnm', 'pbm', 'pgm', 'ppm', 'psd', 'webp', 'converter'],
9+
component: () => import('./image-converter.vue'),
10+
icon: PictureInPicture,
11+
createdAt: new Date('2024-08-15'),
12+
});
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
declare module 'roboto-base64' {
2+
export const normal: string;
3+
}

src/tools/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { tool as outlookParser } from './outlook-parser';
1313
import { tool as fileHasher } from './file-hasher';
1414
import { tool as hexFileConverter } from './hex-file-converter';
1515
import { tool as icalMerger } from './ical-merger';
16+
import { tool as imageConverter } from './image-converter';
1617

1718
import { tool as cssXpathConverter } from './css-xpath-converter';
1819
import { tool as cssSelectorsMemo } from './css-selectors-memo';
@@ -207,6 +208,7 @@ export const toolsByCategory: ToolCategory[] = [
207208
barcodeGenerator,
208209
heicConverter,
209210
icoConverter,
211+
imageConverter,
210212
],
211213
},
212214
{

0 commit comments

Comments
 (0)