diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/composable/downloadBase64.ts | 89 | ||||
-rw-r--r-- | src/tools/base64-file-converter/base64-file-converter.vue | 67 | ||||
-rw-r--r-- | src/utils/base64.test.ts | 13 | ||||
-rw-r--r-- | src/utils/base64.ts | 11 |
4 files changed, 149 insertions, 31 deletions
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; |