diff --git a/components.d.ts b/components.d.ts index f2c3146f..353130ec 100644 --- a/components.d.ts +++ b/components.d.ts @@ -127,29 +127,22 @@ declare module '@vue/runtime-core' { MetaTagGenerator: typeof import('./src/tools/meta-tag-generator/meta-tag-generator.vue')['default'] MimeTypes: typeof import('./src/tools/mime-types/mime-types.vue')['default'] NavbarButtons: typeof import('./src/components/NavbarButtons.vue')['default'] - NCode: typeof import('naive-ui')['NCode'] NCollapseTransition: typeof import('naive-ui')['NCollapseTransition'] NConfigProvider: typeof import('naive-ui')['NConfigProvider'] - NDivider: typeof import('naive-ui')['NDivider'] NEllipsis: typeof import('naive-ui')['NEllipsis'] - NFormItem: typeof import('naive-ui')['NFormItem'] - NGi: typeof import('naive-ui')['NGi'] - NGrid: typeof import('naive-ui')['NGrid'] NH1: typeof import('naive-ui')['NH1'] NH3: typeof import('naive-ui')['NH3'] NIcon: typeof import('naive-ui')['NIcon'] - NInputNumber: typeof import('naive-ui')['NInputNumber'] - NLabel: typeof import('naive-ui')['NLabel'] NLayout: typeof import('naive-ui')['NLayout'] NLayoutSider: typeof import('naive-ui')['NLayoutSider'] NMenu: typeof import('naive-ui')['NMenu'] - NScrollbar: typeof import('naive-ui')['NScrollbar'] NSpin: typeof import('naive-ui')['NSpin'] NumeronymGenerator: typeof import('./src/tools/numeronym-generator/numeronym-generator.vue')['default'] OtpCodeGeneratorAndValidator: typeof import('./src/tools/otp-code-generator-and-validator/otp-code-generator-and-validator.vue')['default'] PasswordStrengthAnalyser: typeof import('./src/tools/password-strength-analyser/password-strength-analyser.vue')['default'] PdfSignatureChecker: typeof import('./src/tools/pdf-signature-checker/pdf-signature-checker.vue')['default'] PdfSignatureDetails: typeof import('./src/tools/pdf-signature-checker/components/pdf-signature-details.vue')['default'] + PdfUnlock: typeof import('./src/tools/pdf-unlock/pdf-unlock.vue')['default'] PercentageCalculator: typeof import('./src/tools/percentage-calculator/percentage-calculator.vue')['default'] PhoneParserAndFormatter: typeof import('./src/tools/phone-parser-and-formatter/phone-parser-and-formatter.vue')['default'] QrCodeGenerator: typeof import('./src/tools/qr-code-generator/qr-code-generator.vue')['default'] diff --git a/package.json b/package.json index 9f39ff1d..c3e6c03b 100644 --- a/package.json +++ b/package.json @@ -38,9 +38,11 @@ "@it-tools/bip39": "^0.0.4", "@it-tools/oggen": "^1.3.0", "@sindresorhus/slugify": "^2.2.1", + "@tiptap/core": "2.1.12", "@tiptap/pm": "2.1.6", "@tiptap/starter-kit": "2.1.6", "@tiptap/vue-3": "2.0.3", + "@types/emscripten": "^1.39.10", "@types/figlet": "^1.5.8", "@vicons/material": "^0.12.0", "@vicons/tabler": "^0.12.0", @@ -64,6 +66,7 @@ "highlight.js": "^11.7.0", "iarna-toml-esm": "^3.0.5", "ibantools": "^4.3.3", + "js-base64": "^3.7.7", "json5": "^2.2.3", "jwt-decode": "^3.1.2", "libphonenumber-js": "^1.10.28", @@ -79,6 +82,7 @@ "pdf-signature-reader": "^1.4.2", "pinia": "^2.0.34", "plausible-tracker": "^0.3.8", + "qpdf-wasm-esm-embedded": "^1.1.1", "qrcode": "^1.5.1", "sql-formatter": "^13.0.0", "ua-parser-js": "^1.0.35", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bd6c38c9..b658639a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,9 @@ dependencies: '@sindresorhus/slugify': specifier: ^2.2.1 version: 2.2.1 + '@tiptap/core': + specifier: 2.1.12 + version: 2.1.12(@tiptap/pm@2.1.6) '@tiptap/pm': specifier: 2.1.6 version: 2.1.6 @@ -23,6 +26,9 @@ dependencies: '@tiptap/vue-3': specifier: 2.0.3 version: 2.0.3(@tiptap/core@2.1.12)(@tiptap/pm@2.1.6)(vue@3.3.4) + '@types/emscripten': + specifier: ^1.39.10 + version: 1.39.10 '@types/figlet': specifier: ^1.5.8 version: 1.5.8 @@ -92,6 +98,9 @@ dependencies: ibantools: specifier: ^4.3.3 version: 4.3.3 + js-base64: + specifier: ^3.7.7 + version: 3.7.7 json5: specifier: ^2.2.3 version: 2.2.3 @@ -137,6 +146,9 @@ dependencies: plausible-tracker: specifier: ^0.3.8 version: 0.3.8 + qpdf-wasm-esm-embedded: + specifier: ^1.1.1 + version: 1.1.1 qrcode: specifier: ^1.5.1 version: 1.5.1 @@ -2901,6 +2913,10 @@ packages: '@types/trusted-types': 2.0.3 dev: true + /@types/emscripten@1.39.10: + resolution: {integrity: sha512-TB/6hBkYQJxsZHSqyeuO1Jt0AB/bW6G7rHt9g7lML7SOF6lbgcHvw/Lr+69iqN0qxgXLhWKScAon73JNnptuDw==} + dev: false + /@types/estree@0.0.39: resolution: {integrity: sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==} dev: true @@ -3351,7 +3367,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.9.0(vue@3.3.4) unhead: 0.5.1 vue: 3.3.4 transitivePeerDependencies: @@ -3993,10 +4009,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.9.0(vue@3.3.4): + resolution: {integrity: sha512-Uud2IWncmAfJvRaFYzv5OHDli+FbOzxiVEQdLCKQKLyhz94PIyFC3CHcH7EDMwIn8NPtD06+PNbC/PiO0LGLtw==} 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 +6488,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'} @@ -7673,6 +7693,12 @@ packages: engines: {node: '>=6'} dev: true + /qpdf-wasm-esm-embedded@1.1.1: + resolution: {integrity: sha512-e6Pb8jRw+VflZQvsS+eOfYF9BqjubvTBnxJc44LhI1btg2WYNKE5/lGMmzWZ2gd9YrdWByf8IdNZw6S7+7t27w==} + dependencies: + '@types/emscripten': 1.39.10 + dev: false + /qrcode@1.5.1: resolution: {integrity: sha512-nS8NJ1Z3md8uTjKtP+SGGhfqmTCs5flU/xR623oI0JX+Wepz9R8UrRVCTBTJm3qGw3rH6jJ6MUHjkDx15cxSSg==} engines: {node: '>=10.13.0'} @@ -9151,8 +9177,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 @@ -9442,6 +9468,7 @@ packages: /workbox-google-analytics@7.0.0: resolution: {integrity: sha512-MEYM1JTn/qiC3DbpvP2BVhyIH+dV/5BjHk756u9VbwuAhu0QHyKscTnisQuz21lfRpOwiS9z4XdqeVAKol0bzg==} + deprecated: It is not compatible with newer versions of GA starting with v4, as long as you are using GAv3 it should be ok, but the package is not longer being maintained dependencies: workbox-background-sync: 7.0.0 workbox-core: 7.0.0 diff --git a/src/composable/downloadBase64.ts b/src/composable/downloadBase64.ts index 37b0428d..367c6a3e 100644 --- a/src/composable/downloadBase64.ts +++ b/src/composable/downloadBase64.ts @@ -1,8 +1,12 @@ -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, +}; const commonMimeTypesSignatures = { 'JVBERi0': 'application/pdf', @@ -36,30 +40,55 @@ function getFileExtensionFromMimeType({ defaultExtension?: string }) { if (mimeType) { - return getExtensionFromMime(mimeType) ?? defaultExtension; + return getExtensionFromMimeType(mimeType) ?? defaultExtension; } return defaultExtension; } -function useDownloadFileFromBase64({ source, filename }: { source: Ref; filename?: string }) { +function downloadFromBase64({ sourceValue, filename, extension, fileMimeType }: +{ sourceValue: string; filename?: string; extension?: string; fileMimeType?: string }) { + if (sourceValue === '') { + throw new Error('Base64 string is empty'); + } + + 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 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(); +} + +function useDownloadFileFromBase64( + { source, filename, extension, fileMimeType }: + { source: Ref; filename?: string; extension?: string; fileMimeType?: string }) { return { download() { - if (source.value === '') { - 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 cleanFileName = filename ?? `file.${getFileExtensionFromMimeType({ mimeType })}`; - - const a = document.createElement('a'); - a.href = base64String; - a.download = cleanFileName; - a.click(); + downloadFromBase64({ sourceValue: source.value, filename, extension, fileMimeType }); + }, + }; +} + +function useDownloadFileFromBase64Refs( + { source, filename, extension }: + { source: Ref; filename?: Ref; extension?: Ref }) { + return { + download() { + downloadFromBase64({ sourceValue: source.value, filename: filename?.value, extension: extension?.value }); }, }; } diff --git a/src/tools/index.ts b/src/tools/index.ts index aa861c93..6bedb3f0 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -81,11 +81,25 @@ import { tool as uuidGenerator } from './uuid-generator'; import { tool as macAddressLookup } from './mac-address-lookup'; import { tool as xmlFormatter } from './xml-formatter'; import { tool as yamlViewer } from './yaml-viewer'; +import { tool as pdfUnlock } from './pdf-unlock'; export const toolsByCategory: ToolCategory[] = [ { name: 'Crypto', - components: [tokenGenerator, hashText, bcrypt, uuidGenerator, ulidGenerator, cypher, bip39, hmacGenerator, rsaKeyPairGenerator, passwordStrengthAnalyser, pdfSignatureChecker], + components: [ + tokenGenerator, + hashText, + bcrypt, + uuidGenerator, + ulidGenerator, + cypher, + bip39, + hmacGenerator, + rsaKeyPairGenerator, + passwordStrengthAnalyser, + pdfSignatureChecker, + pdfUnlock, + ], }, { name: 'Converter', diff --git a/src/tools/pdf-unlock/index.ts b/src/tools/pdf-unlock/index.ts new file mode 100644 index 00000000..04afb91d --- /dev/null +++ b/src/tools/pdf-unlock/index.ts @@ -0,0 +1,12 @@ +import { LockOff } from '@vicons/tabler'; +import { defineTool } from '../tool'; + +export const tool = defineTool({ + name: 'Pdf Decrypt and Unlock', + path: '/pdf-unlock', + description: 'Decrypt a PDF and unlock (remove security permissions)', + keywords: ['pdf', 'unlock', 'decrypt'], + component: () => import('./pdf-unlock.vue'), + icon: LockOff, + createdAt: new Date('2024-01-09'), +}); diff --git a/src/tools/pdf-unlock/pdf-unlock.vue b/src/tools/pdf-unlock/pdf-unlock.vue new file mode 100644 index 00000000..4956254f --- /dev/null +++ b/src/tools/pdf-unlock/pdf-unlock.vue @@ -0,0 +1,87 @@ + + +