Skip to content

Commit d3ee99a

Browse files
committed
feat(new tool): HEIC Converter
Fix CorentinTh#997
1 parent 9eac9cb commit d3ee99a

File tree

8 files changed

+260
-25
lines changed

8 files changed

+260
-25
lines changed

components.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ declare module '@vue/runtime-core' {
8282
GitMemo: typeof import('./src/tools/git-memo/git-memo.vue')['default']
8383
'GitMemo.content': typeof import('./src/tools/git-memo/git-memo.content.md')['default']
8484
HashText: typeof import('./src/tools/hash-text/hash-text.vue')['default']
85+
HeicConverter: typeof import('./src/tools/heic-converter/heic-converter.vue')['default']
8586
HmacGenerator: typeof import('./src/tools/hmac-generator/hmac-generator.vue')['default']
8687
'Home.page': typeof import('./src/pages/Home.page.vue')['default']
8788
HtmlEntities: typeof import('./src/tools/html-entities/html-entities.vue')['default']
@@ -129,6 +130,7 @@ declare module '@vue/runtime-core' {
129130
NavbarButtons: typeof import('./src/components/NavbarButtons.vue')['default']
130131
NCode: typeof import('naive-ui')['NCode']
131132
NCollapseTransition: typeof import('naive-ui')['NCollapseTransition']
133+
NColorPicker: typeof import('naive-ui')['NColorPicker']
132134
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
133135
NDivider: typeof import('naive-ui')['NDivider']
134136
NEllipsis: typeof import('naive-ui')['NEllipsis']
@@ -159,6 +161,7 @@ declare module '@vue/runtime-core' {
159161
RouterLink: typeof import('vue-router')['RouterLink']
160162
RouterView: typeof import('vue-router')['RouterView']
161163
RsaKeyPairGenerator: typeof import('./src/tools/rsa-key-pair-generator/rsa-key-pair-generator.vue')['default']
164+
SafelinkDecoder: typeof import('./src/tools/safelink-decoder/safelink-decoder.vue')['default']
162165
SlugifyString: typeof import('./src/tools/slugify-string/slugify-string.vue')['default']
163166
SpanCopyable: typeof import('./src/components/SpanCopyable.vue')['default']
164167
SqlPrettify: typeof import('./src/tools/sql-prettify/sql-prettify.vue')['default']

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,11 @@
6161
"figlet": "^1.7.0",
6262
"figue": "^1.2.0",
6363
"fuse.js": "^6.6.2",
64+
"heic-convert": "^2.1.0",
6465
"highlight.js": "^11.7.0",
6566
"iarna-toml-esm": "^3.0.5",
6667
"ibantools": "^4.3.3",
68+
"js-base64": "^3.7.7",
6769
"json5": "^2.2.3",
6870
"jwt-decode": "^3.1.2",
6971
"libphonenumber-js": "^1.10.28",

pnpm-lock.yaml

Lines changed: 47 additions & 6 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 & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
1-
import { extension as getExtensionFromMime } from 'mime-types';
1+
import { extension as getExtensionFromMimeType, extension as getMimeTypeFromExtension } from 'mime-types';
22
import type { Ref } from 'vue';
33
import _ from 'lodash';
44

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

712
const commonMimeTypesSignatures = {
813
'JVBERi0': 'application/pdf',
@@ -36,30 +41,78 @@ function getFileExtensionFromMimeType({
3641
defaultExtension?: string
3742
}) {
3843
if (mimeType) {
39-
return getExtensionFromMime(mimeType) ?? defaultExtension;
44+
return getExtensionFromMimeType(mimeType) ?? defaultExtension;
4045
}
4146

4247
return defaultExtension;
4348
}
4449

45-
function useDownloadFileFromBase64({ source, filename }: { source: Ref<string>; filename?: string }) {
46-
return {
47-
download() {
48-
if (source.value === '') {
49-
throw new Error('Base64 string is empty');
50-
}
50+
function downloadFromBase64({ sourceValue, filename, extension, fileMimeType }:
51+
{ sourceValue: string; filename?: string; extension?: string; fileMimeType?: string }) {
52+
if (sourceValue === '') {
53+
throw new Error('Base64 string is empty');
54+
}
5155

52-
const { mimeType } = getMimeTypeFromBase64({ base64String: source.value });
53-
const base64String = mimeType
54-
? source.value
55-
: `data:text/plain;base64,${source.value}`;
56+
const defaultExtension = extension ?? 'txt';
57+
const { mimeType } = getMimeTypeFromBase64({ base64String: sourceValue });
58+
let base64String = sourceValue;
59+
if (!mimeType) {
60+
const targetMimeType = fileMimeType ?? getMimeTypeFromExtension(defaultExtension);
61+
base64String = `data:${targetMimeType};base64,${sourceValue}`;
62+
}
5663

57-
const cleanFileName = filename ?? `file.${getFileExtensionFromMimeType({ mimeType })}`;
64+
const cleanExtension = extension ?? getFileExtensionFromMimeType(
65+
{ mimeType, defaultExtension });
66+
let cleanFileName = filename ?? `file.${cleanExtension}`;
67+
if (extension && !cleanFileName.endsWith(`.${extension}`)) {
68+
cleanFileName = `${cleanFileName}.${cleanExtension}`;
69+
}
5870

59-
const a = document.createElement('a');
60-
a.href = base64String;
61-
a.download = cleanFileName;
62-
a.click();
71+
const a = document.createElement('a');
72+
a.href = base64String;
73+
a.download = cleanFileName;
74+
a.click();
75+
}
76+
77+
function useDownloadFileFromBase64(
78+
{ source, filename, extension, fileMimeType }:
79+
{ source: Ref<string>; filename?: string; extension?: string; fileMimeType?: string }) {
80+
return {
81+
download() {
82+
downloadFromBase64({ sourceValue: source.value, filename, extension, fileMimeType });
6383
},
6484
};
6585
}
86+
87+
function useDownloadFileFromBase64Refs(
88+
{ source, filename, extension }:
89+
{ source: Ref<string>; filename?: Ref<string>; extension?: Ref<string> }) {
90+
return {
91+
download() {
92+
downloadFromBase64({ sourceValue: source.value, filename: filename?.value, extension: extension?.value });
93+
},
94+
};
95+
}
96+
97+
function previewImageFromBase64(base64String: string): HTMLImageElement {
98+
if (base64String === '') {
99+
throw new Error('Base64 string is empty');
100+
}
101+
102+
const img = document.createElement('img');
103+
img.src = base64String;
104+
105+
const container = document.createElement('div');
106+
container.appendChild(img);
107+
108+
const previewContainer = document.getElementById('previewContainer');
109+
if (previewContainer) {
110+
previewContainer.innerHTML = '';
111+
previewContainer.appendChild(container);
112+
}
113+
else {
114+
throw new Error('Preview container element not found');
115+
}
116+
117+
return img;
118+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
declare module 'heic-convert/browser' {
2+
interface ConversionOptions {
3+
/**
4+
* the HEIC file buffer
5+
*/
6+
buffer: ArrayBufferLike;
7+
/**
8+
* output format
9+
*/
10+
format: "JPEG" | "PNG";
11+
/**
12+
* the JPEG compression quality, between 0 and 1
13+
* @default 0.92
14+
*/
15+
quality?: number;
16+
}
17+
18+
interface Convertible {
19+
convert(): Promise<ArrayBuffer>;
20+
}
21+
22+
/** @async */
23+
declare function convert(image: ConversionOptions): Promise<ArrayBuffer>;
24+
declare namespace convert {
25+
/** @async */
26+
function all(image: ConversionOptions): Promise<Convertible[]>;
27+
}
28+
29+
export default convert;
30+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
<script setup lang="ts">
2+
import { Base64 } from 'js-base64';
3+
import heicConvert from 'heic-convert/browser';
4+
import { useDownloadFileFromBase64Refs } from '@/composable/downloadBase64';
5+
6+
const status = ref<'idle' | 'done' | 'error' | 'processing'>('idle');
7+
const file = ref<File | null>(null);
8+
9+
const base64OutputImage = ref('');
10+
const fileName = ref('');
11+
const format = ref('jpg');
12+
const formats = [
13+
{ value: 'jpg', label: 'JPEG' },
14+
{ value: 'png', label: 'PNG' },
15+
];
16+
const { download } = useDownloadFileFromBase64Refs(
17+
{
18+
source: base64OutputImage,
19+
filename: fileName,
20+
});
21+
22+
async function onFileUploaded(uploadedFile: File) {
23+
file.value = uploadedFile;
24+
const fileBuffer = await uploadedFile.arrayBuffer();
25+
26+
fileName.value = `${uploadedFile.name}.${format.value}`;
27+
status.value = 'processing';
28+
try {
29+
let convertFormat;
30+
if (format.value === 'jpg') {
31+
convertFormat = 'JPEG';
32+
}
33+
else if (format.value === 'png') {
34+
convertFormat = 'PNG';
35+
}
36+
else {
37+
throw new Error('unknown format');
38+
}
39+
40+
const outputBuffer = await heicConvert({
41+
buffer: new Uint8Array(fileBuffer),
42+
format: convertFormat as ('JPEG' | 'PNG'),
43+
quality: 0.98,
44+
});
45+
base64OutputImage.value = `data:image/${convertFormat.toLowerCase()};base64,${Base64.fromUint8Array(new Uint8Array(outputBuffer))}`;
46+
47+
status.value = 'done';
48+
49+
download();
50+
}
51+
catch (e) {
52+
status.value = 'error';
53+
}
54+
}
55+
</script>
56+
57+
<template>
58+
<div>
59+
<c-select
60+
v-model:value="format"
61+
:options="formats"
62+
label="Output format"
63+
/>
64+
65+
<div style="flex: 0 0 100%" mt-3>
66+
<div mx-auto max-w-600px>
67+
<c-file-upload
68+
title="Drag and drop a HEIC file here, or click to select a file"
69+
accept=".heic,.heif" @file-upload="onFileUploaded"
70+
/>
71+
</div>
72+
</div>
73+
<div mt-3 flex justify-center>
74+
<img :src="base64OutputImage" max-w-300px>
75+
</div>
76+
77+
<div mt-3 flex justify-center>
78+
<c-alert v-if="status === 'error'" type="error">
79+
An error occured processing {{ fileName }}. HEIC/HEIF is invalid.
80+
</c-alert>
81+
<n-spin
82+
v-if="status === 'processing'"
83+
size="small"
84+
/>
85+
</div>
86+
</div>
87+
</template>

0 commit comments

Comments
 (0)