aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--package.json1
-rw-r--r--pnpm-lock.yaml19
-rw-r--r--src/composable/downloadBase64.ts89
-rw-r--r--src/tools/base64-file-converter/base64-file-converter.vue67
-rw-r--r--src/utils/base64.test.ts13
-rw-r--r--src/utils/base64.ts11
6 files changed, 163 insertions, 37 deletions
diff --git a/package.json b/package.json
index 9f39ff1..c6cb775 100644
--- a/package.json
+++ b/package.json
@@ -64,6 +64,7 @@
"highlight.js": "^11.7.0",
"iarna-toml-esm": "^3.0.5",
"ibantools": "^4.3.3",
+ "js-base64": "^3.7.6",
"json5": "^2.2.3",
"jwt-decode": "^3.1.2",
"libphonenumber-js": "^1.10.28",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index bd6c38c..8619d8c 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -92,6 +92,9 @@ dependencies:
ibantools:
specifier: ^4.3.3
version: 4.3.3
+ js-base64:
+ specifier: ^3.7.6
+ version: 3.7.7
json5:
specifier: ^2.2.3
version: 2.2.3
@@ -3351,7 +3354,7 @@ packages:
dependencies:
'@unhead/dom': 0.5.1
'@unhead/schema': 0.5.1
- '@vueuse/shared': 10.7.2(vue@3.3.4)
+ '@vueuse/shared': 10.8.0(vue@3.3.4)
unhead: 0.5.1
vue: 3.3.4
transitivePeerDependencies:
@@ -3993,10 +3996,10 @@ packages:
- vue
dev: false
- /@vueuse/shared@10.7.2(vue@3.3.4):
- resolution: {integrity: sha512-qFbXoxS44pi2FkgFjPvF4h7c9oMDutpyBdcJdMYIMg9XyXli2meFMuaKn+UMgsClo//Th6+beeCgqweT/79BVA==}
+ /@vueuse/shared@10.8.0(vue@3.3.4):
+ resolution: {integrity: sha512-dUdy6zwHhULGxmr9YUg8e+EnB39gcM4Fe2oKBSrh3cOsV30JcMPtsyuspgFCUo5xxFNaeMf/W2yyKfST7Bg8oQ==}
dependencies:
- vue-demi: 0.14.6(vue@3.3.4)
+ vue-demi: 0.14.7(vue@3.3.4)
transitivePeerDependencies:
- '@vue/composition-api'
- vue
@@ -6472,6 +6475,10 @@ packages:
hasBin: true
dev: true
+ /js-base64@3.7.7:
+ resolution: {integrity: sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==}
+ dev: false
+
/js-beautify@1.14.6:
resolution: {integrity: sha512-GfofQY5zDp+cuHc+gsEXKPpNw2KbPddreEo35O6jT6i0RVK6LhsoYBhq5TvK4/n74wnA0QbK8gGd+jUZwTMKJw==}
engines: {node: '>=10'}
@@ -9151,8 +9158,8 @@ packages:
vue: 3.3.4
dev: false
- /vue-demi@0.14.6(vue@3.3.4):
- resolution: {integrity: sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==}
+ /vue-demi@0.14.7(vue@3.3.4):
+ resolution: {integrity: sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==}
engines: {node: '>=12'}
hasBin: true
requiresBuild: true
diff --git a/src/composable/downloadBase64.ts b/src/composable/downloadBase64.ts
index 37b0428..3bc2022 100644
--- a/src/composable/downloadBase64.ts
+++ b/src/composable/downloadBase64.ts
@@ -1,8 +1,13 @@
-import { extension as getExtensionFromMime } from 'mime-types';
+import { extension as getExtensionFromMimeType, extension as getMimeTypeFromExtension } from 'mime-types';
import type { Ref } from 'vue';
import _ from 'lodash';
-export { getMimeTypeFromBase64, useDownloadFileFromBase64 };
+export {
+ getMimeTypeFromBase64,
+ getMimeTypeFromExtension, getExtensionFromMimeType,
+ useDownloadFileFromBase64, useDownloadFileFromBase64Refs,
+ previewImageFromBase64,
+};
const commonMimeTypesSignatures = {
'JVBERi0': 'application/pdf',
@@ -36,30 +41,78 @@ function getFileExtensionFromMimeType({
defaultExtension?: string
}) {
if (mimeType) {
- return getExtensionFromMime(mimeType) ?? defaultExtension;
+ return getExtensionFromMimeType(mimeType) ?? defaultExtension;
}
return defaultExtension;
}
-function useDownloadFileFromBase64({ source, filename }: { source: Ref<string>; filename?: string }) {
- return {
- download() {
- if (source.value === '') {
- throw new Error('Base64 string is empty');
- }
+function downloadFromBase64({ sourceValue, filename, extension, fileMimeType }:
+{ sourceValue: string; filename?: string; extension?: string; fileMimeType?: string }) {
+ if (sourceValue === '') {
+ throw new Error('Base64 string is empty');
+ }
- const { mimeType } = getMimeTypeFromBase64({ base64String: source.value });
- const base64String = mimeType
- ? source.value
- : `data:text/plain;base64,${source.value}`;
+ const defaultExtension = extension ?? 'txt';
+ const { mimeType } = getMimeTypeFromBase64({ base64String: sourceValue });
+ let base64String = sourceValue;
+ if (!mimeType) {
+ const targetMimeType = fileMimeType ?? getMimeTypeFromExtension(defaultExtension);
+ base64String = `data:${targetMimeType};base64,${sourceValue}`;
+ }
- const cleanFileName = filename ?? `file.${getFileExtensionFromMimeType({ mimeType })}`;
+ const cleanExtension = extension ?? getFileExtensionFromMimeType(
+ { mimeType, defaultExtension });
+ let cleanFileName = filename ?? `file.${cleanExtension}`;
+ if (extension && !cleanFileName.endsWith(`.${extension}`)) {
+ cleanFileName = `${cleanFileName}.${cleanExtension}`;
+ }
- const a = document.createElement('a');
- a.href = base64String;
- a.download = cleanFileName;
- a.click();
+ const a = document.createElement('a');
+ a.href = base64String;
+ a.download = cleanFileName;
+ a.click();
+}
+
+function useDownloadFileFromBase64(
+ { source, filename, extension, fileMimeType }:
+ { source: Ref<string>; filename?: string; extension?: string; fileMimeType?: string }) {
+ return {
+ download() {
+ downloadFromBase64({ sourceValue: source.value, filename, extension, fileMimeType });
},
};
}
+
+function useDownloadFileFromBase64Refs(
+ { source, filename, extension }:
+ { source: Ref<string>; filename?: Ref<string>; extension?: Ref<string> }) {
+ return {
+ download() {
+ downloadFromBase64({ sourceValue: source.value, filename: filename?.value, extension: extension?.value });
+ },
+ };
+}
+
+function previewImageFromBase64(base64String: string): HTMLImageElement {
+ if (base64String === '') {
+ throw new Error('Base64 string is empty');
+ }
+
+ const img = document.createElement('img');
+ img.src = base64String;
+
+ const container = document.createElement('div');
+ container.appendChild(img);
+
+ const previewContainer = document.getElementById('previewContainer');
+ if (previewContainer) {
+ previewContainer.innerHTML = '';
+ previewContainer.appendChild(container);
+ }
+ else {
+ throw new Error('Preview container element not found');
+ }
+
+ return img;
+}
diff --git a/src/tools/base64-file-converter/base64-file-converter.vue b/src/tools/base64-file-converter/base64-file-converter.vue
index 377625b..a489f9a 100644
--- a/src/tools/base64-file-converter/base64-file-converter.vue
+++ b/src/tools/base64-file-converter/base64-file-converter.vue
@@ -2,12 +2,19 @@
import { useBase64 } from '@vueuse/core';
import type { Ref } from 'vue';
import { useCopy } from '@/composable/copy';
-import { useDownloadFileFromBase64 } from '@/composable/downloadBase64';
+import { getExtensionFromMimeType, getMimeTypeFromBase64, previewImageFromBase64, useDownloadFileFromBase64Refs } from '@/composable/downloadBase64';
import { useValidation } from '@/composable/validation';
import { isValidBase64 } from '@/utils/base64';
+const fileName = ref('file');
+const fileExtension = ref('');
const base64Input = ref('');
-const { download } = useDownloadFileFromBase64({ source: base64Input });
+const { download } = useDownloadFileFromBase64Refs(
+ {
+ source: base64Input,
+ filename: fileName,
+ extension: fileExtension,
+ });
const base64InputValidation = useValidation({
source: base64Input,
rules: [
@@ -18,6 +25,35 @@ const base64InputValidation = useValidation({
],
});
+watch(
+ base64Input,
+ (newValue, _) => {
+ const { mimeType } = getMimeTypeFromBase64({ base64String: newValue });
+ if (mimeType) {
+ fileExtension.value = getExtensionFromMimeType(mimeType) || fileExtension.value;
+ }
+ },
+);
+
+function previewImage() {
+ if (!base64InputValidation.isValid) {
+ return;
+ }
+ try {
+ const image = previewImageFromBase64(base64Input.value);
+ image.style.maxWidth = '100%';
+ image.style.maxHeight = '400px';
+ const previewContainer = document.getElementById('previewContainer');
+ if (previewContainer) {
+ previewContainer.innerHTML = '';
+ previewContainer.appendChild(image);
+ }
+ }
+ catch (_) {
+ //
+ }
+}
+
function downloadFile() {
if (!base64InputValidation.isValid) {
return;
@@ -44,6 +80,24 @@ async function onUpload(file: File) {
<template>
<c-card title="Base64 to file">
+ <n-grid cols="3" x-gap="12">
+ <n-gi span="2">
+ <c-input-text
+ v-model:value="fileName"
+ label="File Name"
+ placeholder="Download filename"
+ mb-2
+ />
+ </n-gi>
+ <n-gi>
+ <c-input-text
+ v-model:value="fileExtension"
+ label="Extension"
+ placeholder="Extension"
+ mb-2
+ />
+ </n-gi>
+ </n-grid>
<c-input-text
v-model:value="base64Input"
multiline
@@ -53,7 +107,14 @@ async function onUpload(file: File) {
mb-2
/>
- <div flex justify-center>
+ <div flex justify-center py-2>
+ <div id="previewContainer" />
+ </div>
+
+ <div flex justify-center gap-3>
+ <c-button :disabled="base64Input === '' || !base64InputValidation.isValid" @click="previewImage()">
+ Preview image
+ </c-button>
<c-button :disabled="base64Input === '' || !base64InputValidation.isValid" @click="downloadFile()">
Download file
</c-button>
diff --git a/src/utils/base64.test.ts b/src/utils/base64.test.ts
index 994f1b1..51d1523 100644
--- a/src/utils/base64.test.ts
+++ b/src/utils/base64.test.ts
@@ -38,7 +38,8 @@ describe('base64 utils', () => {
it('should throw for incorrect base64 string', () => {
expect(() => base64ToText('a')).to.throw('Incorrect base64 string');
- expect(() => base64ToText(' ')).to.throw('Incorrect base64 string');
+ // should not really be false because trimming of space is now implied
+ // expect(() => base64ToText(' ')).to.throw('Incorrect base64 string');
expect(() => base64ToText('é')).to.throw('Incorrect base64 string');
// missing final '='
expect(() => base64ToText('bG9yZW0gaXBzdW0')).to.throw('Incorrect base64 string');
@@ -56,17 +57,17 @@ describe('base64 utils', () => {
it('should return false for incorrect base64 string', () => {
expect(isValidBase64('a')).to.eql(false);
- expect(isValidBase64(' ')).to.eql(false);
expect(isValidBase64('é')).to.eql(false);
expect(isValidBase64('data:text/plain;notbase64,YQ==')).to.eql(false);
// missing final '='
expect(isValidBase64('bG9yZW0gaXBzdW0')).to.eql(false);
});
- it('should return false for untrimmed correct base64 string', () => {
- expect(isValidBase64('bG9yZW0gaXBzdW0= ')).to.eql(false);
- expect(isValidBase64(' LTE=')).to.eql(false);
- expect(isValidBase64(' YQ== ')).to.eql(false);
+ it('should return true for untrimmed correct base64 string', () => {
+ expect(isValidBase64('bG9yZW0gaXBzdW0= ')).to.eql(true);
+ expect(isValidBase64(' LTE=')).to.eql(true);
+ expect(isValidBase64(' YQ== ')).to.eql(true);
+ expect(isValidBase64(' ')).to.eql(true);
});
});
diff --git a/src/utils/base64.ts b/src/utils/base64.ts
index 16912ee..44e59f4 100644
--- a/src/utils/base64.ts
+++ b/src/utils/base64.ts
@@ -1,7 +1,9 @@
+import { Base64 } from 'js-base64';
+
export { textToBase64, base64ToText, isValidBase64, removePotentialDataAndMimePrefix };
function textToBase64(str: string, { makeUrlSafe = false }: { makeUrlSafe?: boolean } = {}) {
- const encoded = window.btoa(str);
+ const encoded = Base64.encode(str);
return makeUrlSafe ? makeUriSafe(encoded) : encoded;
}
@@ -16,7 +18,7 @@ function base64ToText(str: string, { makeUrlSafe = false }: { makeUrlSafe?: bool
}
try {
- return window.atob(cleanStr);
+ return Base64.decode(cleanStr);
}
catch (_) {
throw new Error('Incorrect base64 string');
@@ -34,10 +36,11 @@ function isValidBase64(str: string, { makeUrlSafe = false }: { makeUrlSafe?: boo
}
try {
+ const reEncodedBase64 = Base64.fromUint8Array(Base64.toUint8Array(cleanStr));
if (makeUrlSafe) {
- return removePotentialPadding(window.btoa(window.atob(cleanStr))) === cleanStr;
+ return removePotentialPadding(reEncodedBase64) === cleanStr;
}
- return window.btoa(window.atob(cleanStr)) === cleanStr;
+ return reEncodedBase64 === cleanStr.replace(/\s/g, '');
}
catch (err) {
return false;