feat: expand conversion matrix to everything-to-everything
- Documents: full cross-format (PDF, DOCX, HTML, MD, TXT, RTF) via pdfjs-dist, docx, mammoth, jsPDF, marked - Images: add TIFF and ICO encoding (custom binary encoders for browser support) - Data: add TOML support via smol-toml, all formats cross-convert - Media: fix webm codec (VP8+Vorbis instead of libx264), expand video-to-audio extraction matrix - Add missing MIME types (docx, rtf, toml, tiff, ico, opus, wma, flv, wmv, m4v) - Remove unused pdf-lib dependency - Fix pdfjs-dist TextItem type error with proper type guard
This commit is contained in:
Generated
+377
-54
@@ -10,6 +10,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ffmpeg/ffmpeg": "^0.12.15",
|
"@ffmpeg/ffmpeg": "^0.12.15",
|
||||||
"@ffmpeg/util": "^0.12.2",
|
"@ffmpeg/util": "^0.12.2",
|
||||||
|
"docx": "^9.6.0",
|
||||||
"fast-xml-parser": "^5.4.2",
|
"fast-xml-parser": "^5.4.2",
|
||||||
"framer-motion": "^12.35.2",
|
"framer-motion": "^12.35.2",
|
||||||
"html2canvas-pro": "^2.0.2",
|
"html2canvas-pro": "^2.0.2",
|
||||||
@@ -20,9 +21,10 @@
|
|||||||
"marked": "^17.0.4",
|
"marked": "^17.0.4",
|
||||||
"next": "16.1.6",
|
"next": "16.1.6",
|
||||||
"papaparse": "^5.5.3",
|
"papaparse": "^5.5.3",
|
||||||
"pdf-lib": "^1.17.1",
|
"pdfjs-dist": "^5.5.207",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-dom": "19.2.3"
|
"react-dom": "19.2.3",
|
||||||
|
"smol-toml": "^1.6.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
@@ -1074,6 +1076,256 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@napi-rs/canvas": {
|
||||||
|
"version": "0.1.96",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.96.tgz",
|
||||||
|
"integrity": "sha512-6NNmNxvoJKeucVjxaaRUt3La2i5jShgiAbaY3G/72s1Vp3U06XPrAIxkAjBxpDcamEn/t+WJ4OOlGmvILo4/Ew==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"workspaces": [
|
||||||
|
"e2e/*"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@napi-rs/canvas-android-arm64": "0.1.96",
|
||||||
|
"@napi-rs/canvas-darwin-arm64": "0.1.96",
|
||||||
|
"@napi-rs/canvas-darwin-x64": "0.1.96",
|
||||||
|
"@napi-rs/canvas-linux-arm-gnueabihf": "0.1.96",
|
||||||
|
"@napi-rs/canvas-linux-arm64-gnu": "0.1.96",
|
||||||
|
"@napi-rs/canvas-linux-arm64-musl": "0.1.96",
|
||||||
|
"@napi-rs/canvas-linux-riscv64-gnu": "0.1.96",
|
||||||
|
"@napi-rs/canvas-linux-x64-gnu": "0.1.96",
|
||||||
|
"@napi-rs/canvas-linux-x64-musl": "0.1.96",
|
||||||
|
"@napi-rs/canvas-win32-arm64-msvc": "0.1.96",
|
||||||
|
"@napi-rs/canvas-win32-x64-msvc": "0.1.96"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/canvas-android-arm64": {
|
||||||
|
"version": "0.1.96",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.96.tgz",
|
||||||
|
"integrity": "sha512-ew1sPrN3dGdZ3L4FoohPfnjq0f9/Jk7o+wP7HkQZokcXgIUD6FIyICEWGhMYzv53j63wUcPvZeAwgewX58/egg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/canvas-darwin-arm64": {
|
||||||
|
"version": "0.1.96",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.96.tgz",
|
||||||
|
"integrity": "sha512-Q/wOXZ5PzTqpdmA5eUOcegCf4Go/zz3aZ5DlzSeDpOjFmfwMKh8EzLAoweQ+mJVagcHQyzoJhaTEnrO68TNyNg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/canvas-darwin-x64": {
|
||||||
|
"version": "0.1.96",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.96.tgz",
|
||||||
|
"integrity": "sha512-UrXiQz28tQEvGM1qvyptewOAfmUrrd5+wvi6Rzjj2VprZI8iZ2KIvBD2lTTG1bVF95AbeDeG7PJA0D9sLKaOFA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/canvas-linux-arm-gnueabihf": {
|
||||||
|
"version": "0.1.96",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.96.tgz",
|
||||||
|
"integrity": "sha512-I90ODxweD8aEP6XKU/NU+biso95MwCtQ2F46dUvhec1HesFi0tq/tAJkYic/1aBSiO/1kGKmSeD1B0duOHhEHQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/canvas-linux-arm64-gnu": {
|
||||||
|
"version": "0.1.96",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.96.tgz",
|
||||||
|
"integrity": "sha512-Dx/0+RFV++w3PcRy+4xNXkghhXjA5d0Mw1bs95emn5Llinp1vihMaA6WJt3oYv2LAHc36+gnrhIBsPhUyI2SGw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/canvas-linux-arm64-musl": {
|
||||||
|
"version": "0.1.96",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.96.tgz",
|
||||||
|
"integrity": "sha512-UvOi7fii3IE2KDfEfhh8m+LpzSRvhGK7o1eho99M2M0HTik11k3GX+2qgVx9EtujN3/bhFFS1kSO3+vPMaJ0Mg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/canvas-linux-riscv64-gnu": {
|
||||||
|
"version": "0.1.96",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.96.tgz",
|
||||||
|
"integrity": "sha512-MBSukhGCQ5nRtf9NbFYWOU080yqkZU1PbuH4o1ROvB4CbPl12fchDR35tU83Wz8gWIM9JTn99lBn9DenPIv7Ig==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/canvas-linux-x64-gnu": {
|
||||||
|
"version": "0.1.96",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.96.tgz",
|
||||||
|
"integrity": "sha512-I/ccu2SstyKiV3HIeVzyBIWfrJo8cN7+MSQZPnabewWV6hfJ2nY7Df2WqOHmobBRUw84uGR6zfQHsUEio/m5Vg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/canvas-linux-x64-musl": {
|
||||||
|
"version": "0.1.96",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.96.tgz",
|
||||||
|
"integrity": "sha512-H3uov7qnTl73GDT4h52lAqpJPsl1tIUyNPWJyhQ6gHakohNqqRq3uf80+NEpzcytKGEOENP1wX3yGwZxhjiWEQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/canvas-win32-arm64-msvc": {
|
||||||
|
"version": "0.1.96",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-0.1.96.tgz",
|
||||||
|
"integrity": "sha512-ATp6Y+djOjYtkfV/VRH7CZ8I1MEtkUQBmKUbuWw5zWEHHqfL0cEcInE4Cxgx7zkNAhEdBbnH8HMVrqNp+/gwxA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/canvas-win32-x64-msvc": {
|
||||||
|
"version": "0.1.96",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.96.tgz",
|
||||||
|
"integrity": "sha512-UYGdTltVd+Z8mcIuoqGmAXXUvwH5CLf2M6mIB5B0/JmX5J041jETjqtSYl7gN+aj3k1by/SG6sS0hAwCqyK7zw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@napi-rs/wasm-runtime": {
|
"node_modules/@napi-rs/wasm-runtime": {
|
||||||
"version": "0.2.12",
|
"version": "0.2.12",
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz",
|
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz",
|
||||||
@@ -1279,36 +1531,6 @@
|
|||||||
"node": ">=12.4.0"
|
"node": ">=12.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@pdf-lib/standard-fonts": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@pdf-lib/standard-fonts/-/standard-fonts-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"pako": "^1.0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@pdf-lib/standard-fonts/node_modules/pako": {
|
|
||||||
"version": "1.0.11",
|
|
||||||
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
|
|
||||||
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
|
|
||||||
"license": "(MIT AND Zlib)"
|
|
||||||
},
|
|
||||||
"node_modules/@pdf-lib/upng": {
|
|
||||||
"version": "1.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@pdf-lib/upng/-/upng-1.0.1.tgz",
|
|
||||||
"integrity": "sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"pako": "^1.0.10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@pdf-lib/upng/node_modules/pako": {
|
|
||||||
"version": "1.0.11",
|
|
||||||
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
|
|
||||||
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
|
|
||||||
"license": "(MIT AND Zlib)"
|
|
||||||
},
|
|
||||||
"node_modules/@rtsao/scc": {
|
"node_modules/@rtsao/scc": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
|
||||||
@@ -3022,6 +3244,56 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/docx": {
|
||||||
|
"version": "9.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/docx/-/docx-9.6.0.tgz",
|
||||||
|
"integrity": "sha512-y6EaJJMDvt4P7wgGQB9KsZf4wsRkQMJfkc9LlNufRshggI5BT35hGNkXBCAeEoI3MLMwApKguxzjdqqVcBCqNA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "^25.2.3",
|
||||||
|
"hash.js": "^1.1.7",
|
||||||
|
"jszip": "^3.10.1",
|
||||||
|
"nanoid": "^5.1.3",
|
||||||
|
"xml": "^1.0.1",
|
||||||
|
"xml-js": "^1.6.8"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/docx/node_modules/@types/node": {
|
||||||
|
"version": "25.3.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.5.tgz",
|
||||||
|
"integrity": "sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~7.18.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/docx/node_modules/nanoid": {
|
||||||
|
"version": "5.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.6.tgz",
|
||||||
|
"integrity": "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ai"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"nanoid": "bin/nanoid.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18 || >=20"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/docx/node_modules/undici-types": {
|
||||||
|
"version": "7.18.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
|
||||||
|
"integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/dompurify": {
|
"node_modules/dompurify": {
|
||||||
"version": "3.3.2",
|
"version": "3.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.2.tgz",
|
||||||
@@ -4208,6 +4480,16 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/hash.js": {
|
||||||
|
"version": "1.1.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
|
||||||
|
"integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"inherits": "^2.0.3",
|
||||||
|
"minimalistic-assert": "^1.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/hasown": {
|
"node_modules/hasown": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||||
@@ -5369,6 +5651,12 @@
|
|||||||
"node": ">=8.6"
|
"node": ">=8.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/minimalistic-assert": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/minimatch": {
|
"node_modules/minimatch": {
|
||||||
"version": "3.1.5",
|
"version": "3.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||||
@@ -5555,6 +5843,13 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/node-readable-to-web-readable-stream": {
|
||||||
|
"version": "0.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-readable-to-web-readable-stream/-/node-readable-to-web-readable-stream-0.4.2.tgz",
|
||||||
|
"integrity": "sha512-/cMZNI34v//jUTrI+UIo4ieHAB5EZRY/+7OmXZgBxaWBMcW2tGdceIw06RFxWxrKZ5Jp3sI2i5TsRo+CBhtVLQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/node-releases": {
|
"node_modules/node-releases": {
|
||||||
"version": "2.0.36",
|
"version": "2.0.36",
|
||||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz",
|
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz",
|
||||||
@@ -5820,30 +6115,19 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/pdf-lib": {
|
"node_modules/pdfjs-dist": {
|
||||||
"version": "1.17.1",
|
"version": "5.5.207",
|
||||||
"resolved": "https://registry.npmjs.org/pdf-lib/-/pdf-lib-1.17.1.tgz",
|
"resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.5.207.tgz",
|
||||||
"integrity": "sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==",
|
"integrity": "sha512-WMqqw06w1vUt9ZfT0gOFhMf3wHsWhaCrxGrckGs5Cci6ybDW87IvPaOd2pnBwT6BJuP/CzXDZxjFgmSULLdsdw==",
|
||||||
"license": "MIT",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"engines": {
|
||||||
"@pdf-lib/standard-fonts": "^1.0.0",
|
"node": ">=20.19.0 || >=22.13.0 || >=24"
|
||||||
"@pdf-lib/upng": "^1.0.1",
|
},
|
||||||
"pako": "^1.0.11",
|
"optionalDependencies": {
|
||||||
"tslib": "^1.11.1"
|
"@napi-rs/canvas": "^0.1.95",
|
||||||
|
"node-readable-to-web-readable-stream": "^0.4.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/pdf-lib/node_modules/pako": {
|
|
||||||
"version": "1.0.11",
|
|
||||||
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
|
|
||||||
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
|
|
||||||
"license": "(MIT AND Zlib)"
|
|
||||||
},
|
|
||||||
"node_modules/pdf-lib/node_modules/tslib": {
|
|
||||||
"version": "1.14.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
|
||||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
|
|
||||||
"license": "0BSD"
|
|
||||||
},
|
|
||||||
"node_modules/performance-now": {
|
"node_modules/performance-now": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
|
||||||
@@ -6225,6 +6509,15 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/sax": {
|
||||||
|
"version": "1.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/sax/-/sax-1.5.0.tgz",
|
||||||
|
"integrity": "sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA==",
|
||||||
|
"license": "BlueOak-1.0.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=11.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/scheduler": {
|
"node_modules/scheduler": {
|
||||||
"version": "0.27.0",
|
"version": "0.27.0",
|
||||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
|
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
|
||||||
@@ -6453,6 +6746,18 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/smol-toml": {
|
||||||
|
"version": "1.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.6.0.tgz",
|
||||||
|
"integrity": "sha512-4zemZi0HvTnYwLfrpk/CF9LOd9Lt87kAt50GnqhMpyF9U3poDAP2+iukq2bZsO/ufegbYehBkqINbsWxj4l4cw==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/cyyynthia"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/source-map-js": {
|
"node_modules/source-map-js": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
@@ -7218,6 +7523,24 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/xml": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/xml-js": {
|
||||||
|
"version": "1.6.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz",
|
||||||
|
"integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"sax": "^1.2.4"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"xml-js": "bin/cli.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/xmlbuilder": {
|
"node_modules/xmlbuilder": {
|
||||||
"version": "10.1.1",
|
"version": "10.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-10.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-10.1.1.tgz",
|
||||||
|
|||||||
+4
-2
@@ -11,6 +11,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ffmpeg/ffmpeg": "^0.12.15",
|
"@ffmpeg/ffmpeg": "^0.12.15",
|
||||||
"@ffmpeg/util": "^0.12.2",
|
"@ffmpeg/util": "^0.12.2",
|
||||||
|
"docx": "^9.6.0",
|
||||||
"fast-xml-parser": "^5.4.2",
|
"fast-xml-parser": "^5.4.2",
|
||||||
"framer-motion": "^12.35.2",
|
"framer-motion": "^12.35.2",
|
||||||
"html2canvas-pro": "^2.0.2",
|
"html2canvas-pro": "^2.0.2",
|
||||||
@@ -21,9 +22,10 @@
|
|||||||
"marked": "^17.0.4",
|
"marked": "^17.0.4",
|
||||||
"next": "16.1.6",
|
"next": "16.1.6",
|
||||||
"papaparse": "^5.5.3",
|
"papaparse": "^5.5.3",
|
||||||
"pdf-lib": "^1.17.1",
|
"pdfjs-dist": "^5.5.207",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-dom": "19.2.3"
|
"react-dom": "19.2.3",
|
||||||
|
"smol-toml": "^1.6.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
|
|||||||
+45
-42
@@ -1,57 +1,59 @@
|
|||||||
import { FileCategory } from '@/types';
|
import { FileCategory } from '@/types';
|
||||||
|
|
||||||
const IMAGE_CONVERSIONS: Record<string, string[]> = {
|
const IMAGE_CONVERSIONS: Record<string, string[]> = {
|
||||||
png: ['jpg', 'webp', 'gif', 'bmp', 'avif'],
|
png: ['jpg', 'webp', 'gif', 'bmp', 'avif', 'tiff', 'ico'],
|
||||||
jpg: ['png', 'webp', 'gif', 'bmp', 'avif'],
|
jpg: ['png', 'webp', 'gif', 'bmp', 'avif', 'tiff', 'ico'],
|
||||||
jpeg: ['png', 'webp', 'gif', 'bmp', 'avif'],
|
jpeg: ['png', 'webp', 'gif', 'bmp', 'avif', 'tiff', 'ico'],
|
||||||
webp: ['png', 'jpg', 'gif', 'bmp', 'avif'],
|
webp: ['png', 'jpg', 'gif', 'bmp', 'avif', 'tiff', 'ico'],
|
||||||
gif: ['png', 'jpg', 'webp', 'bmp'],
|
gif: ['png', 'jpg', 'webp', 'bmp', 'avif', 'tiff'],
|
||||||
bmp: ['png', 'jpg', 'webp', 'gif'],
|
bmp: ['png', 'jpg', 'webp', 'gif', 'avif', 'tiff'],
|
||||||
tiff: ['png', 'jpg', 'webp'],
|
tiff: ['png', 'jpg', 'webp', 'gif', 'bmp', 'avif'],
|
||||||
tif: ['png', 'jpg', 'webp'],
|
tif: ['png', 'jpg', 'webp', 'gif', 'bmp', 'avif'],
|
||||||
avif: ['png', 'jpg', 'webp'],
|
avif: ['png', 'jpg', 'webp', 'gif', 'bmp', 'tiff'],
|
||||||
svg: ['png', 'jpg', 'webp'],
|
svg: ['png', 'jpg', 'webp', 'gif', 'bmp', 'avif', 'tiff'],
|
||||||
ico: ['png', 'jpg', 'webp'],
|
ico: ['png', 'jpg', 'webp', 'gif', 'bmp'],
|
||||||
};
|
};
|
||||||
|
|
||||||
const DOCUMENT_CONVERSIONS: Record<string, string[]> = {
|
const DOCUMENT_CONVERSIONS: Record<string, string[]> = {
|
||||||
docx: ['html', 'txt', 'pdf'],
|
pdf: ['txt', 'html', 'md', 'docx'],
|
||||||
md: ['html', 'pdf', 'txt'],
|
docx: ['pdf', 'html', 'txt', 'md'],
|
||||||
html: ['pdf', 'txt', 'md'],
|
md: ['html', 'pdf', 'txt', 'docx'],
|
||||||
htm: ['pdf', 'txt', 'md'],
|
html: ['pdf', 'txt', 'md', 'docx'],
|
||||||
txt: ['pdf', 'html', 'md'],
|
htm: ['pdf', 'txt', 'md', 'docx'],
|
||||||
pdf: ['txt'],
|
txt: ['pdf', 'html', 'md', 'docx'],
|
||||||
|
rtf: ['txt', 'html', 'md', 'pdf', 'docx'],
|
||||||
};
|
};
|
||||||
|
|
||||||
const AUDIO_CONVERSIONS: Record<string, string[]> = {
|
const AUDIO_CONVERSIONS: Record<string, string[]> = {
|
||||||
mp3: ['wav', 'ogg', 'aac', 'flac', 'm4a'],
|
mp3: ['wav', 'ogg', 'aac', 'flac', 'm4a', 'opus'],
|
||||||
wav: ['mp3', 'ogg', 'aac', 'flac', 'm4a'],
|
wav: ['mp3', 'ogg', 'aac', 'flac', 'm4a', 'opus'],
|
||||||
flac: ['mp3', 'wav', 'ogg', 'aac', 'm4a'],
|
flac: ['mp3', 'wav', 'ogg', 'aac', 'm4a', 'opus'],
|
||||||
ogg: ['mp3', 'wav', 'aac', 'flac', 'm4a'],
|
ogg: ['mp3', 'wav', 'aac', 'flac', 'm4a', 'opus'],
|
||||||
aac: ['mp3', 'wav', 'ogg', 'flac', 'm4a'],
|
aac: ['mp3', 'wav', 'ogg', 'flac', 'm4a', 'opus'],
|
||||||
m4a: ['mp3', 'wav', 'ogg', 'flac', 'aac'],
|
m4a: ['mp3', 'wav', 'ogg', 'flac', 'aac', 'opus'],
|
||||||
wma: ['mp3', 'wav', 'ogg', 'flac'],
|
wma: ['mp3', 'wav', 'ogg', 'flac', 'aac', 'm4a'],
|
||||||
opus: ['mp3', 'wav', 'ogg', 'flac'],
|
opus: ['mp3', 'wav', 'ogg', 'flac', 'aac', 'm4a'],
|
||||||
};
|
};
|
||||||
|
|
||||||
const VIDEO_CONVERSIONS: Record<string, string[]> = {
|
const VIDEO_CONVERSIONS: Record<string, string[]> = {
|
||||||
mp4: ['webm', 'avi', 'mov', 'gif', 'mp3'],
|
mp4: ['webm', 'avi', 'mov', 'mkv', 'gif', 'mp3', 'wav', 'ogg', 'aac', 'flac'],
|
||||||
webm: ['mp4', 'avi', 'mov', 'gif', 'mp3'],
|
webm: ['mp4', 'avi', 'mov', 'mkv', 'gif', 'mp3', 'wav', 'ogg', 'aac', 'flac'],
|
||||||
avi: ['mp4', 'webm', 'mov', 'gif', 'mp3'],
|
avi: ['mp4', 'webm', 'mov', 'mkv', 'gif', 'mp3', 'wav', 'ogg', 'aac', 'flac'],
|
||||||
mov: ['mp4', 'webm', 'avi', 'gif', 'mp3'],
|
mov: ['mp4', 'webm', 'avi', 'mkv', 'gif', 'mp3', 'wav', 'ogg', 'aac', 'flac'],
|
||||||
mkv: ['mp4', 'webm', 'avi', 'gif', 'mp3'],
|
mkv: ['mp4', 'webm', 'avi', 'mov', 'gif', 'mp3', 'wav', 'ogg', 'aac', 'flac'],
|
||||||
flv: ['mp4', 'webm', 'avi', 'mp3'],
|
flv: ['mp4', 'webm', 'avi', 'mov', 'mkv', 'gif', 'mp3', 'wav', 'ogg', 'aac', 'flac'],
|
||||||
wmv: ['mp4', 'webm', 'avi', 'mp3'],
|
wmv: ['mp4', 'webm', 'avi', 'mov', 'mkv', 'gif', 'mp3', 'wav', 'ogg', 'aac', 'flac'],
|
||||||
m4v: ['mp4', 'webm', 'avi', 'mp3'],
|
m4v: ['mp4', 'webm', 'avi', 'mov', 'mkv', 'gif', 'mp3', 'wav', 'ogg', 'aac', 'flac'],
|
||||||
};
|
};
|
||||||
|
|
||||||
const DATA_CONVERSIONS: Record<string, string[]> = {
|
const DATA_CONVERSIONS: Record<string, string[]> = {
|
||||||
csv: ['json', 'xml', 'yaml', 'tsv'],
|
csv: ['json', 'xml', 'yaml', 'tsv', 'toml'],
|
||||||
json: ['csv', 'xml', 'yaml'],
|
json: ['csv', 'xml', 'yaml', 'tsv', 'toml'],
|
||||||
xml: ['json', 'csv', 'yaml'],
|
xml: ['json', 'csv', 'yaml', 'tsv', 'toml'],
|
||||||
yaml: ['json', 'csv', 'xml'],
|
yaml: ['json', 'csv', 'xml', 'tsv', 'toml'],
|
||||||
yml: ['json', 'csv', 'xml'],
|
yml: ['json', 'csv', 'xml', 'tsv', 'toml'],
|
||||||
tsv: ['csv', 'json', 'xml', 'yaml'],
|
tsv: ['csv', 'json', 'xml', 'yaml', 'toml'],
|
||||||
|
toml: ['json', 'csv', 'xml', 'yaml', 'tsv'],
|
||||||
};
|
};
|
||||||
|
|
||||||
const ALL_CONVERSIONS: Record<FileCategory, Record<string, string[]>> = {
|
const ALL_CONVERSIONS: Record<FileCategory, Record<string, string[]>> = {
|
||||||
@@ -75,14 +77,15 @@ export function getDefaultTarget(category: FileCategory, extension: string): str
|
|||||||
// Images → WebP (modern, smaller)
|
// Images → WebP (modern, smaller)
|
||||||
png: 'webp', jpg: 'webp', jpeg: 'webp', gif: 'webp',
|
png: 'webp', jpg: 'webp', jpeg: 'webp', gif: 'webp',
|
||||||
bmp: 'png', tiff: 'png', tif: 'png', avif: 'png', svg: 'png', ico: 'png',
|
bmp: 'png', tiff: 'png', tif: 'png', avif: 'png', svg: 'png', ico: 'png',
|
||||||
// Documents → PDF
|
// Documents → PDF (except PDF → DOCX)
|
||||||
docx: 'pdf', md: 'html', html: 'pdf', txt: 'pdf', pdf: 'txt',
|
docx: 'pdf', md: 'html', html: 'pdf', htm: 'pdf', txt: 'pdf',
|
||||||
|
pdf: 'docx', rtf: 'docx',
|
||||||
// Audio → MP3
|
// Audio → MP3
|
||||||
wav: 'mp3', flac: 'mp3', ogg: 'mp3', aac: 'mp3', m4a: 'mp3', wma: 'mp3', opus: 'mp3', mp3: 'wav',
|
wav: 'mp3', flac: 'mp3', ogg: 'mp3', aac: 'mp3', m4a: 'mp3', wma: 'mp3', opus: 'mp3', mp3: 'wav',
|
||||||
// Video → MP4
|
// Video → MP4
|
||||||
avi: 'mp4', mov: 'mp4', mkv: 'mp4', flv: 'mp4', wmv: 'mp4', m4v: 'mp4', mp4: 'webm', webm: 'mp4',
|
avi: 'mp4', mov: 'mp4', mkv: 'mp4', flv: 'mp4', wmv: 'mp4', m4v: 'mp4', mp4: 'webm', webm: 'mp4',
|
||||||
// Data → JSON
|
// Data → JSON
|
||||||
csv: 'json', xml: 'json', yaml: 'json', yml: 'json', tsv: 'csv', json: 'csv',
|
csv: 'json', xml: 'json', yaml: 'json', yml: 'json', tsv: 'csv', json: 'csv', toml: 'json',
|
||||||
};
|
};
|
||||||
|
|
||||||
return defaults[extension] || formats[0];
|
return defaults[extension] || formats[0];
|
||||||
|
|||||||
@@ -52,6 +52,17 @@ function yamlToJson(text: string): unknown {
|
|||||||
return yaml.load(text);
|
return yaml.load(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function tomlToJson(text: string): Promise<unknown> {
|
||||||
|
const TOML = await import('smol-toml');
|
||||||
|
return TOML.parse(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function jsonToToml(data: unknown): Promise<string> {
|
||||||
|
const TOML = await import('smol-toml');
|
||||||
|
const obj = typeof data === 'string' ? JSON.parse(data) : data;
|
||||||
|
return TOML.stringify(obj as Record<string, unknown>);
|
||||||
|
}
|
||||||
|
|
||||||
async function toIntermediate(file: File, ext: string): Promise<unknown> {
|
async function toIntermediate(file: File, ext: string): Promise<unknown> {
|
||||||
const text = await readFileAsText(file);
|
const text = await readFileAsText(file);
|
||||||
|
|
||||||
@@ -67,12 +78,14 @@ async function toIntermediate(file: File, ext: string): Promise<unknown> {
|
|||||||
case 'yaml':
|
case 'yaml':
|
||||||
case 'yml':
|
case 'yml':
|
||||||
return yamlToJson(text);
|
return yamlToJson(text);
|
||||||
|
case 'toml':
|
||||||
|
return tomlToJson(text);
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unsupported source format: ${ext}`);
|
throw new Error(`Unsupported source format: ${ext}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function fromIntermediate(data: unknown, targetFormat: string): string {
|
async function fromIntermediate(data: unknown, targetFormat: string): Promise<string> {
|
||||||
switch (targetFormat) {
|
switch (targetFormat) {
|
||||||
case 'json':
|
case 'json':
|
||||||
return JSON.stringify(data, null, 2);
|
return JSON.stringify(data, null, 2);
|
||||||
@@ -85,6 +98,8 @@ function fromIntermediate(data: unknown, targetFormat: string): string {
|
|||||||
case 'yaml':
|
case 'yaml':
|
||||||
case 'yml':
|
case 'yml':
|
||||||
return jsonToYaml(data);
|
return jsonToYaml(data);
|
||||||
|
case 'toml':
|
||||||
|
return jsonToToml(data);
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unsupported target format: ${targetFormat}`);
|
throw new Error(`Unsupported target format: ${targetFormat}`);
|
||||||
}
|
}
|
||||||
@@ -101,7 +116,7 @@ export async function convertData(
|
|||||||
const intermediate = await toIntermediate(file, ext);
|
const intermediate = await toIntermediate(file, ext);
|
||||||
onProgress?.(60);
|
onProgress?.(60);
|
||||||
|
|
||||||
const output = fromIntermediate(intermediate, targetFormat);
|
const output = await fromIntermediate(intermediate, targetFormat);
|
||||||
onProgress?.(90);
|
onProgress?.(90);
|
||||||
|
|
||||||
const blob = new Blob([output], { type: getMimeType(targetFormat) });
|
const blob = new Blob([output], { type: getMimeType(targetFormat) });
|
||||||
|
|||||||
@@ -26,12 +26,6 @@ async function readFileAsArrayBuffer(file: File): Promise<ArrayBuffer> {
|
|||||||
|
|
||||||
/* ============================================
|
/* ============================================
|
||||||
Styled HTML document wrapper
|
Styled HTML document wrapper
|
||||||
|
|
||||||
This is used for ALL HTML output and as the
|
|
||||||
intermediate step for PDF rendering. Embeds
|
|
||||||
full CSS so the document looks correct both
|
|
||||||
as a standalone .html file and when rendered
|
|
||||||
to PDF via jsPDF.html().
|
|
||||||
============================================ */
|
============================================ */
|
||||||
|
|
||||||
function wrapInStyledHtml(bodyHtml: string, title: string): string {
|
function wrapInStyledHtml(bodyHtml: string, title: string): string {
|
||||||
@@ -42,9 +36,7 @@ function wrapInStyledHtml(bodyHtml: string, title: string): string {
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>${escapeHtml(title)}</title>
|
<title>${escapeHtml(title)}</title>
|
||||||
<style>
|
<style>
|
||||||
/* Reset */
|
|
||||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Helvetica Neue', Arial, sans-serif;
|
font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Helvetica Neue', Arial, sans-serif;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
@@ -55,132 +47,40 @@ function wrapInStyledHtml(bodyHtml: string, title: string): string {
|
|||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Headings */
|
|
||||||
h1, h2, h3, h4, h5, h6 {
|
h1, h2, h3, h4, h5, h6 {
|
||||||
margin-top: 1.4em;
|
margin-top: 1.4em; margin-bottom: 0.6em; font-weight: 700; line-height: 1.3; color: #111111;
|
||||||
margin-bottom: 0.6em;
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 1.3;
|
|
||||||
color: #111111;
|
|
||||||
}
|
}
|
||||||
h1 { font-size: 2em; border-bottom: 2px solid #e5e5e5; padding-bottom: 0.3em; }
|
h1 { font-size: 2em; border-bottom: 2px solid #e5e5e5; padding-bottom: 0.3em; }
|
||||||
h2 { font-size: 1.5em; border-bottom: 1px solid #eeeeee; padding-bottom: 0.25em; }
|
h2 { font-size: 1.5em; border-bottom: 1px solid #eeeeee; padding-bottom: 0.25em; }
|
||||||
h3 { font-size: 1.25em; }
|
h3 { font-size: 1.25em; }
|
||||||
h4 { font-size: 1.1em; }
|
h4 { font-size: 1.1em; }
|
||||||
h5, h6 { font-size: 1em; color: #555555; }
|
h5, h6 { font-size: 1em; color: #555555; }
|
||||||
|
|
||||||
/* Paragraphs & inline */
|
|
||||||
p { margin-bottom: 1em; }
|
p { margin-bottom: 1em; }
|
||||||
strong, b { font-weight: 700; }
|
strong, b { font-weight: 700; }
|
||||||
em, i { font-style: italic; }
|
em, i { font-style: italic; }
|
||||||
u { text-decoration: underline; }
|
|
||||||
s, strike, del { text-decoration: line-through; color: #888; }
|
|
||||||
small { font-size: 0.85em; }
|
|
||||||
sup { vertical-align: super; font-size: 0.75em; }
|
|
||||||
sub { vertical-align: sub; font-size: 0.75em; }
|
|
||||||
mark { background: #fff3b0; padding: 0.1em 0.2em; border-radius: 2px; }
|
|
||||||
abbr { text-decoration: underline dotted; cursor: help; }
|
|
||||||
|
|
||||||
/* Links */
|
|
||||||
a { color: #0066cc; text-decoration: underline; }
|
a { color: #0066cc; text-decoration: underline; }
|
||||||
a:hover { color: #004499; }
|
|
||||||
|
|
||||||
/* Lists */
|
|
||||||
ul, ol { margin-bottom: 1em; padding-left: 2em; }
|
ul, ol { margin-bottom: 1em; padding-left: 2em; }
|
||||||
ul ul, ol ol, ul ol, ol ul { margin-bottom: 0; }
|
|
||||||
li { margin-bottom: 0.3em; }
|
li { margin-bottom: 0.3em; }
|
||||||
li > p { margin-bottom: 0.3em; }
|
|
||||||
|
|
||||||
/* Blockquote */
|
|
||||||
blockquote {
|
blockquote {
|
||||||
margin: 1em 0;
|
margin: 1em 0; padding: 0.8em 1.2em; border-left: 4px solid #0066cc;
|
||||||
padding: 0.8em 1.2em;
|
background: #f6f8fa; color: #333; font-style: italic;
|
||||||
border-left: 4px solid #0066cc;
|
|
||||||
background: #f6f8fa;
|
|
||||||
color: #333;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
}
|
||||||
blockquote p:last-child { margin-bottom: 0; }
|
blockquote p:last-child { margin-bottom: 0; }
|
||||||
|
|
||||||
/* Code */
|
|
||||||
code {
|
code {
|
||||||
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em; background: #f0f0f0; padding: 0.15em 0.4em; border-radius: 3px; color: #c7254e;
|
||||||
background: #f0f0f0;
|
|
||||||
padding: 0.15em 0.4em;
|
|
||||||
border-radius: 3px;
|
|
||||||
color: #c7254e;
|
|
||||||
}
|
}
|
||||||
pre {
|
pre {
|
||||||
margin: 1em 0;
|
margin: 1em 0; padding: 1em; background: #f6f8fa; border: 1px solid #e1e4e8;
|
||||||
padding: 1em;
|
border-radius: 6px; overflow-x: auto; font-size: 0.9em; line-height: 1.5;
|
||||||
background: #f6f8fa;
|
|
||||||
border: 1px solid #e1e4e8;
|
|
||||||
border-radius: 6px;
|
|
||||||
overflow-x: auto;
|
|
||||||
font-size: 0.9em;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
pre code {
|
|
||||||
background: none;
|
|
||||||
padding: 0;
|
|
||||||
border-radius: 0;
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Tables */
|
|
||||||
table {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
margin: 1em 0;
|
|
||||||
font-size: 0.95em;
|
|
||||||
}
|
|
||||||
th, td {
|
|
||||||
padding: 8px 12px;
|
|
||||||
border: 1px solid #d0d7de;
|
|
||||||
text-align: left;
|
|
||||||
vertical-align: top;
|
|
||||||
}
|
|
||||||
th {
|
|
||||||
background: #f6f8fa;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #111;
|
|
||||||
}
|
}
|
||||||
|
pre code { background: none; padding: 0; border-radius: 0; color: inherit; }
|
||||||
|
table { width: 100%; border-collapse: collapse; margin: 1em 0; font-size: 0.95em; }
|
||||||
|
th, td { padding: 8px 12px; border: 1px solid #d0d7de; text-align: left; vertical-align: top; }
|
||||||
|
th { background: #f6f8fa; font-weight: 700; color: #111; }
|
||||||
tr:nth-child(even) { background: #fafbfc; }
|
tr:nth-child(even) { background: #fafbfc; }
|
||||||
caption {
|
hr { border: none; border-top: 1px solid #e5e5e5; margin: 2em 0; }
|
||||||
caption-side: bottom;
|
img { max-width: 100%; height: auto; border-radius: 4px; margin: 1em 0; }
|
||||||
padding: 8px;
|
|
||||||
font-size: 0.9em;
|
|
||||||
color: #666;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Horizontal rule */
|
|
||||||
hr {
|
|
||||||
border: none;
|
|
||||||
border-top: 1px solid #e5e5e5;
|
|
||||||
margin: 2em 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Images embedded in documents */
|
|
||||||
img {
|
|
||||||
max-width: 100%;
|
|
||||||
height: auto;
|
|
||||||
border-radius: 4px;
|
|
||||||
margin: 1em 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Definition lists */
|
|
||||||
dl { margin-bottom: 1em; }
|
|
||||||
dt { font-weight: 700; margin-top: 0.5em; }
|
|
||||||
dd { margin-left: 2em; margin-bottom: 0.5em; }
|
|
||||||
|
|
||||||
/* Figure */
|
|
||||||
figure { margin: 1.5em 0; text-align: center; }
|
|
||||||
figcaption { font-size: 0.9em; color: #666; margin-top: 0.5em; font-style: italic; }
|
|
||||||
|
|
||||||
/* First element shouldn't have top margin */
|
|
||||||
body > *:first-child { margin-top: 0; }
|
body > *:first-child { margin-top: 0; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
@@ -198,6 +98,313 @@ function escapeHtml(text: string): string {
|
|||||||
.replace(/"/g, '"');
|
.replace(/"/g, '"');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
PDF text extraction via pdfjs-dist
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
async function pdfToText(file: File): Promise<string> {
|
||||||
|
const pdfjsLib = await import('pdfjs-dist');
|
||||||
|
|
||||||
|
// Use the bundled worker
|
||||||
|
pdfjsLib.GlobalWorkerOptions.workerSrc = `https://unpkg.com/pdfjs-dist@${pdfjsLib.version}/build/pdf.worker.min.mjs`;
|
||||||
|
|
||||||
|
const arrayBuffer = await readFileAsArrayBuffer(file);
|
||||||
|
const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
|
||||||
|
|
||||||
|
const textParts: string[] = [];
|
||||||
|
|
||||||
|
for (let i = 1; i <= pdf.numPages; i++) {
|
||||||
|
const page = await pdf.getPage(i);
|
||||||
|
const content = await page.getTextContent();
|
||||||
|
const pageText = content.items
|
||||||
|
.filter((item) => 'str' in item)
|
||||||
|
.map((item) => (item as { str: string }).str)
|
||||||
|
.join(' ');
|
||||||
|
if (pageText.trim()) {
|
||||||
|
textParts.push(pageText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (textParts.length === 0) {
|
||||||
|
return `[This PDF contains no extractable text — it may be image-based/scanned.]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return textParts.join('\n\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
PDF → HTML
|
||||||
|
Extracts text per page, wraps in styled HTML
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
async function pdfToHtml(file: File): Promise<string> {
|
||||||
|
const text = await pdfToText(file);
|
||||||
|
const paragraphs = text.split(/\n\n+/).filter(Boolean);
|
||||||
|
const bodyHtml = paragraphs.map((p) => `<p>${escapeHtml(p)}</p>`).join('\n');
|
||||||
|
return wrapInStyledHtml(bodyHtml, file.name.replace(/\.pdf$/i, ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
PDF → Markdown
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
async function pdfToMarkdown(file: File): Promise<string> {
|
||||||
|
const text = await pdfToText(file);
|
||||||
|
// Attempt to detect headings (ALL CAPS lines, short lines)
|
||||||
|
const lines = text.split('\n');
|
||||||
|
const mdLines: string[] = [];
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
mdLines.push('');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Heuristic: short all-caps lines are likely headings
|
||||||
|
if (trimmed.length < 80 && trimmed === trimmed.toUpperCase() && /[A-Z]/.test(trimmed)) {
|
||||||
|
mdLines.push(`## ${trimmed}`);
|
||||||
|
} else {
|
||||||
|
mdLines.push(trimmed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mdLines.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
PDF → DOCX
|
||||||
|
Extracts text, builds DOCX using docx package
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
async function pdfToDocx(file: File): Promise<Blob> {
|
||||||
|
const text = await pdfToText(file);
|
||||||
|
return textToDocx(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
Text/HTML/MD → DOCX generation using docx pkg
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
async function textToDocx(text: string): Promise<Blob> {
|
||||||
|
const docx = await import('docx');
|
||||||
|
const paragraphs = text.split(/\n\n+/).filter(Boolean);
|
||||||
|
|
||||||
|
const children = paragraphs.map(
|
||||||
|
(p) =>
|
||||||
|
new docx.Paragraph({
|
||||||
|
children: [new docx.TextRun({ text: p, size: 24 })],
|
||||||
|
spacing: { after: 200 },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const doc = new docx.Document({
|
||||||
|
sections: [{ children }],
|
||||||
|
});
|
||||||
|
|
||||||
|
return await docx.Packer.toBlob(doc);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function htmlToDocx(html: string): Promise<Blob> {
|
||||||
|
// Convert HTML to plain text, then build DOCX
|
||||||
|
const plainText = htmlToText(html);
|
||||||
|
return textToDocx(plainText);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function markdownToDocx(mdText: string): Promise<Blob> {
|
||||||
|
const docx = await import('docx');
|
||||||
|
const lines = mdText.split('\n');
|
||||||
|
const children: InstanceType<typeof docx.Paragraph>[] = [];
|
||||||
|
|
||||||
|
let i = 0;
|
||||||
|
while (i < lines.length) {
|
||||||
|
const line = lines[i];
|
||||||
|
|
||||||
|
// Headings
|
||||||
|
const h1Match = line.match(/^#\s+(.+)/);
|
||||||
|
const h2Match = line.match(/^##\s+(.+)/);
|
||||||
|
const h3Match = line.match(/^###\s+(.+)/);
|
||||||
|
const h4Match = line.match(/^####\s+(.+)/);
|
||||||
|
|
||||||
|
if (h1Match) {
|
||||||
|
children.push(
|
||||||
|
new docx.Paragraph({
|
||||||
|
children: [new docx.TextRun({ text: h1Match[1], bold: true, size: 48 })],
|
||||||
|
heading: docx.HeadingLevel.HEADING_1,
|
||||||
|
spacing: { after: 200 },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else if (h2Match) {
|
||||||
|
children.push(
|
||||||
|
new docx.Paragraph({
|
||||||
|
children: [new docx.TextRun({ text: h2Match[1], bold: true, size: 36 })],
|
||||||
|
heading: docx.HeadingLevel.HEADING_2,
|
||||||
|
spacing: { after: 160 },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else if (h3Match) {
|
||||||
|
children.push(
|
||||||
|
new docx.Paragraph({
|
||||||
|
children: [new docx.TextRun({ text: h3Match[1], bold: true, size: 28 })],
|
||||||
|
heading: docx.HeadingLevel.HEADING_3,
|
||||||
|
spacing: { after: 120 },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else if (h4Match) {
|
||||||
|
children.push(
|
||||||
|
new docx.Paragraph({
|
||||||
|
children: [new docx.TextRun({ text: h4Match[1], bold: true, size: 24 })],
|
||||||
|
heading: docx.HeadingLevel.HEADING_4,
|
||||||
|
spacing: { after: 100 },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Unordered list
|
||||||
|
else if (line.match(/^[-*+]\s+/)) {
|
||||||
|
children.push(
|
||||||
|
new docx.Paragraph({
|
||||||
|
children: parseInlineMarkdown(docx, line.replace(/^[-*+]\s+/, '')),
|
||||||
|
bullet: { level: 0 },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Ordered list
|
||||||
|
else if (line.match(/^\d+\.\s+/)) {
|
||||||
|
children.push(
|
||||||
|
new docx.Paragraph({
|
||||||
|
children: parseInlineMarkdown(docx, line.replace(/^\d+\.\s+/, '')),
|
||||||
|
numbering: { reference: 'default-numbering', level: 0 },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Blockquote
|
||||||
|
else if (line.startsWith('>')) {
|
||||||
|
children.push(
|
||||||
|
new docx.Paragraph({
|
||||||
|
children: [
|
||||||
|
new docx.TextRun({
|
||||||
|
text: line.replace(/^>\s*/, ''),
|
||||||
|
italics: true,
|
||||||
|
color: '555555',
|
||||||
|
size: 24,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
indent: { left: 720 },
|
||||||
|
border: {
|
||||||
|
left: { style: docx.BorderStyle.SINGLE, size: 6, color: '0066cc', space: 10 },
|
||||||
|
},
|
||||||
|
spacing: { after: 120 },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Horizontal rule
|
||||||
|
else if (line.match(/^(-{3,}|\*{3,}|_{3,})$/)) {
|
||||||
|
children.push(
|
||||||
|
new docx.Paragraph({
|
||||||
|
children: [],
|
||||||
|
border: {
|
||||||
|
bottom: { style: docx.BorderStyle.SINGLE, size: 1, color: 'CCCCCC', space: 10 },
|
||||||
|
},
|
||||||
|
spacing: { before: 200, after: 200 },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Code block
|
||||||
|
else if (line.startsWith('```')) {
|
||||||
|
i++;
|
||||||
|
const codeLines: string[] = [];
|
||||||
|
while (i < lines.length && !lines[i].startsWith('```')) {
|
||||||
|
codeLines.push(lines[i]);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
children.push(
|
||||||
|
new docx.Paragraph({
|
||||||
|
children: [
|
||||||
|
new docx.TextRun({
|
||||||
|
text: codeLines.join('\n'),
|
||||||
|
font: 'Courier New',
|
||||||
|
size: 20,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
shading: { type: docx.ShadingType.SOLID, color: 'F6F8FA' },
|
||||||
|
spacing: { before: 120, after: 120 },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Empty line
|
||||||
|
else if (line.trim() === '') {
|
||||||
|
children.push(new docx.Paragraph({ children: [], spacing: { after: 120 } }));
|
||||||
|
}
|
||||||
|
// Regular paragraph
|
||||||
|
else {
|
||||||
|
children.push(
|
||||||
|
new docx.Paragraph({
|
||||||
|
children: parseInlineMarkdown(docx, line),
|
||||||
|
spacing: { after: 160 },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
const doc = new docx.Document({
|
||||||
|
numbering: {
|
||||||
|
config: [
|
||||||
|
{
|
||||||
|
reference: 'default-numbering',
|
||||||
|
levels: [
|
||||||
|
{
|
||||||
|
level: 0,
|
||||||
|
format: docx.LevelFormat.DECIMAL,
|
||||||
|
text: '%1.',
|
||||||
|
alignment: docx.AlignmentType.START,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
sections: [{ children }],
|
||||||
|
});
|
||||||
|
|
||||||
|
return await docx.Packer.toBlob(doc);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
function parseInlineMarkdown(docx: any, text: string): any[] {
|
||||||
|
const runs: any[] = [];
|
||||||
|
// Regex to detect **bold**, *italic*, `code`, ~~strikethrough~~
|
||||||
|
const regex = /(\*\*(.+?)\*\*|\*(.+?)\*|`(.+?)`|~~(.+?)~~|([^*`~]+))/g;
|
||||||
|
let match;
|
||||||
|
|
||||||
|
while ((match = regex.exec(text)) !== null) {
|
||||||
|
if (match[2]) {
|
||||||
|
// Bold
|
||||||
|
runs.push(new docx.TextRun({ text: match[2], bold: true, size: 24 }));
|
||||||
|
} else if (match[3]) {
|
||||||
|
// Italic
|
||||||
|
runs.push(new docx.TextRun({ text: match[3], italics: true, size: 24 }));
|
||||||
|
} else if (match[4]) {
|
||||||
|
// Code
|
||||||
|
runs.push(
|
||||||
|
new docx.TextRun({ text: match[4], font: 'Courier New', size: 22, color: 'C7254E' })
|
||||||
|
);
|
||||||
|
} else if (match[5]) {
|
||||||
|
// Strikethrough
|
||||||
|
runs.push(new docx.TextRun({ text: match[5], strike: true, size: 24 }));
|
||||||
|
} else if (match[6]) {
|
||||||
|
// Plain text
|
||||||
|
runs.push(new docx.TextRun({ text: match[6], size: 24 }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (runs.length === 0) {
|
||||||
|
runs.push(new docx.TextRun({ text, size: 24 }));
|
||||||
|
}
|
||||||
|
|
||||||
|
return runs;
|
||||||
|
}
|
||||||
|
/* eslint-enable @typescript-eslint/no-explicit-any */
|
||||||
|
|
||||||
/* ============================================
|
/* ============================================
|
||||||
Source → HTML conversions
|
Source → HTML conversions
|
||||||
============================================ */
|
============================================ */
|
||||||
@@ -205,9 +412,7 @@ function escapeHtml(text: string): string {
|
|||||||
async function docxToHtml(file: File): Promise<string> {
|
async function docxToHtml(file: File): Promise<string> {
|
||||||
const mammoth = await import('mammoth');
|
const mammoth = await import('mammoth');
|
||||||
const arrayBuffer = await readFileAsArrayBuffer(file);
|
const arrayBuffer = await readFileAsArrayBuffer(file);
|
||||||
const result = await mammoth.convertToHtml({
|
const result = await mammoth.convertToHtml({ arrayBuffer });
|
||||||
arrayBuffer,
|
|
||||||
});
|
|
||||||
return result.value;
|
return result.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,6 +423,11 @@ async function docxToText(file: File): Promise<string> {
|
|||||||
return result.value;
|
return result.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function docxToMarkdown(file: File): Promise<string> {
|
||||||
|
const bodyHtml = await docxToHtml(file);
|
||||||
|
return htmlToMarkdown(bodyHtml);
|
||||||
|
}
|
||||||
|
|
||||||
async function markdownToHtml(text: string): Promise<string> {
|
async function markdownToHtml(text: string): Promise<string> {
|
||||||
const { marked } = await import('marked');
|
const { marked } = await import('marked');
|
||||||
return await marked(text);
|
return await marked(text);
|
||||||
@@ -230,7 +440,6 @@ function htmlToText(html: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function htmlToMarkdown(html: string): string {
|
function htmlToMarkdown(html: string): string {
|
||||||
// Parse properly using DOMParser for reliable conversion
|
|
||||||
const parser = new DOMParser();
|
const parser = new DOMParser();
|
||||||
const doc = parser.parseFromString(html, 'text/html');
|
const doc = parser.parseFromString(html, 'text/html');
|
||||||
|
|
||||||
@@ -238,7 +447,6 @@ function htmlToMarkdown(html: string): string {
|
|||||||
if (node.nodeType === Node.TEXT_NODE) {
|
if (node.nodeType === Node.TEXT_NODE) {
|
||||||
return node.textContent || '';
|
return node.textContent || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node.nodeType !== Node.ELEMENT_NODE) return '';
|
if (node.nodeType !== Node.ELEMENT_NODE) return '';
|
||||||
|
|
||||||
const el = node as Element;
|
const el = node as Element;
|
||||||
@@ -293,32 +501,20 @@ function htmlToMarkdown(html: string): string {
|
|||||||
case 'table': {
|
case 'table': {
|
||||||
const rows = Array.from(el.querySelectorAll('tr'));
|
const rows = Array.from(el.querySelectorAll('tr'));
|
||||||
if (rows.length === 0) return children;
|
if (rows.length === 0) return children;
|
||||||
|
|
||||||
const tableData: string[][] = rows.map(row =>
|
const tableData: string[][] = rows.map(row =>
|
||||||
Array.from(row.querySelectorAll('th, td')).map(cell => walk(cell).trim())
|
Array.from(row.querySelectorAll('th, td')).map(cell => walk(cell).trim())
|
||||||
);
|
);
|
||||||
|
|
||||||
if (tableData.length === 0) return '';
|
if (tableData.length === 0) return '';
|
||||||
|
|
||||||
const colCount = Math.max(...tableData.map(r => r.length));
|
const colCount = Math.max(...tableData.map(r => r.length));
|
||||||
const colWidths = Array.from({ length: colCount }, (_, i) =>
|
const colWidths = Array.from({ length: colCount }, (_, i) =>
|
||||||
Math.max(3, ...tableData.map(r => (r[i] || '').length))
|
Math.max(3, ...tableData.map(r => (r[i] || '').length))
|
||||||
);
|
);
|
||||||
|
|
||||||
const formatRow = (row: string[]) =>
|
const formatRow = (row: string[]) =>
|
||||||
'| ' + colWidths.map((w, i) => (row[i] || '').padEnd(w)).join(' | ') + ' |';
|
'| ' + colWidths.map((w, i) => (row[i] || '').padEnd(w)).join(' | ') + ' |';
|
||||||
|
|
||||||
const separator = '| ' + colWidths.map(w => '-'.repeat(w)).join(' | ') + ' |';
|
const separator = '| ' + colWidths.map(w => '-'.repeat(w)).join(' | ') + ' |';
|
||||||
|
|
||||||
const lines = [formatRow(tableData[0]), separator, ...tableData.slice(1).map(formatRow)];
|
const lines = [formatRow(tableData[0]), separator, ...tableData.slice(1).map(formatRow)];
|
||||||
return lines.join('\n') + '\n\n';
|
return lines.join('\n') + '\n\n';
|
||||||
}
|
}
|
||||||
case 'div':
|
|
||||||
case 'section':
|
|
||||||
case 'article':
|
|
||||||
case 'main':
|
|
||||||
case 'span':
|
|
||||||
return children;
|
|
||||||
default:
|
default:
|
||||||
return children;
|
return children;
|
||||||
}
|
}
|
||||||
@@ -327,35 +523,62 @@ function htmlToMarkdown(html: string): string {
|
|||||||
return walk(doc.body).replace(/\n{3,}/g, '\n\n').trim();
|
return walk(doc.body).replace(/\n{3,}/g, '\n\n').trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
RTF → text (basic extraction)
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
function rtfToText(rtf: string): string {
|
||||||
|
// Strip RTF control words and groups, extract plain text
|
||||||
|
let text = rtf;
|
||||||
|
// Remove header up to first \pard
|
||||||
|
const pardIndex = text.indexOf('\\pard');
|
||||||
|
if (pardIndex > 0) {
|
||||||
|
// Keep content from first \pard onwards but strip the \pard itself
|
||||||
|
text = text.substring(pardIndex);
|
||||||
|
}
|
||||||
|
// Handle common RTF escapes
|
||||||
|
text = text.replace(/\\par\b/g, '\n');
|
||||||
|
text = text.replace(/\\tab\b/g, '\t');
|
||||||
|
text = text.replace(/\\line\b/g, '\n');
|
||||||
|
text = text.replace(/\\\n/g, '\n');
|
||||||
|
text = text.replace(/\\pard[^\\]*/g, '');
|
||||||
|
// Remove {\*\...} groups (destinations we don't care about)
|
||||||
|
text = text.replace(/\{\\\*\\[^}]*\}/g, '');
|
||||||
|
// Remove remaining RTF commands (\word or \wordN)
|
||||||
|
text = text.replace(/\\[a-z]+\d*\s?/gi, '');
|
||||||
|
// Remove braces
|
||||||
|
text = text.replace(/[{}]/g, '');
|
||||||
|
// Handle unicode escapes \\uN
|
||||||
|
text = text.replace(/\\u(\d+)\??/g, (_, code) => String.fromCharCode(parseInt(code)));
|
||||||
|
// Handle hex escapes \\'XX
|
||||||
|
text = text.replace(/\\'([0-9a-fA-F]{2})/g, (_, hex) =>
|
||||||
|
String.fromCharCode(parseInt(hex, 16))
|
||||||
|
);
|
||||||
|
// Clean up
|
||||||
|
text = text.replace(/\r\n/g, '\n');
|
||||||
|
text = text.replace(/\n{3,}/g, '\n\n');
|
||||||
|
return text.trim();
|
||||||
|
}
|
||||||
|
|
||||||
/* ============================================
|
/* ============================================
|
||||||
HTML → PDF via jsPDF.html()
|
HTML → PDF via jsPDF.html()
|
||||||
|
|
||||||
Renders a styled HTML document into a real
|
|
||||||
PDF by injecting it into a hidden DOM container
|
|
||||||
and using jsPDF's html() method (backed by
|
|
||||||
html2canvas) to capture the visual rendering.
|
|
||||||
============================================ */
|
============================================ */
|
||||||
|
|
||||||
async function renderHtmlToPdf(htmlContent: string): Promise<Blob> {
|
async function renderHtmlToPdf(htmlContent: string): Promise<Blob> {
|
||||||
const { jsPDF } = await import('jspdf');
|
const { jsPDF } = await import('jspdf');
|
||||||
// html2canvas-pro is imported for its side-effect:
|
|
||||||
// jsPDF.html() looks for it on the window/global scope
|
|
||||||
const html2canvas = (await import('html2canvas-pro')).default;
|
const html2canvas = (await import('html2canvas-pro')).default;
|
||||||
|
|
||||||
// Create a hidden container for rendering
|
|
||||||
const container = document.createElement('div');
|
const container = document.createElement('div');
|
||||||
container.style.position = 'fixed';
|
container.style.position = 'fixed';
|
||||||
container.style.left = '-10000px';
|
container.style.left = '-10000px';
|
||||||
container.style.top = '0';
|
container.style.top = '0';
|
||||||
container.style.width = '794px'; // A4 width in px at 96dpi
|
container.style.width = '794px';
|
||||||
container.style.background = '#ffffff';
|
container.style.background = '#ffffff';
|
||||||
container.style.zIndex = '-9999';
|
container.style.zIndex = '-9999';
|
||||||
|
|
||||||
// Parse the HTML and inject just the body + styles
|
|
||||||
const parser = new DOMParser();
|
const parser = new DOMParser();
|
||||||
const parsed = parser.parseFromString(htmlContent, 'text/html');
|
const parsed = parser.parseFromString(htmlContent, 'text/html');
|
||||||
|
|
||||||
// Apply styles inline
|
|
||||||
const styleEl = parsed.querySelector('style');
|
const styleEl = parsed.querySelector('style');
|
||||||
const bodyContent = parsed.body.innerHTML;
|
const bodyContent = parsed.body.innerHTML;
|
||||||
|
|
||||||
@@ -375,19 +598,15 @@ async function renderHtmlToPdf(htmlContent: string): Promise<Blob> {
|
|||||||
container.appendChild(content);
|
container.appendChild(content);
|
||||||
|
|
||||||
document.body.appendChild(container);
|
document.body.appendChild(container);
|
||||||
|
|
||||||
// Wait for fonts/images to load
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// A4 dimensions in mm: 210 x 297
|
|
||||||
const pdfWidth = 210;
|
const pdfWidth = 210;
|
||||||
const pdfHeight = 297;
|
const pdfHeight = 297;
|
||||||
const margin = 15; // mm
|
const margin = 15;
|
||||||
|
|
||||||
// Capture the rendered content as a canvas
|
|
||||||
const canvas = await html2canvas(content, {
|
const canvas = await html2canvas(content, {
|
||||||
scale: 2, // Higher resolution
|
scale: 2,
|
||||||
useCORS: true,
|
useCORS: true,
|
||||||
allowTaint: true,
|
allowTaint: true,
|
||||||
backgroundColor: '#ffffff',
|
backgroundColor: '#ffffff',
|
||||||
@@ -395,7 +614,6 @@ async function renderHtmlToPdf(htmlContent: string): Promise<Blob> {
|
|||||||
windowWidth: 794,
|
windowWidth: 794,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Calculate how the content maps to PDF pages
|
|
||||||
const imgWidth = pdfWidth - margin * 2;
|
const imgWidth = pdfWidth - margin * 2;
|
||||||
const imgHeight = (canvas.height * imgWidth) / canvas.width;
|
const imgHeight = (canvas.height * imgWidth) / canvas.width;
|
||||||
|
|
||||||
@@ -403,57 +621,27 @@ async function renderHtmlToPdf(htmlContent: string): Promise<Blob> {
|
|||||||
const pageContentHeight = pdfHeight - margin * 2;
|
const pageContentHeight = pdfHeight - margin * 2;
|
||||||
|
|
||||||
if (imgHeight <= pageContentHeight) {
|
if (imgHeight <= pageContentHeight) {
|
||||||
// Single page — fits entirely
|
doc.addImage(canvas.toDataURL('image/jpeg', 0.95), 'JPEG', margin, margin, imgWidth, imgHeight);
|
||||||
doc.addImage(
|
|
||||||
canvas.toDataURL('image/jpeg', 0.95),
|
|
||||||
'JPEG',
|
|
||||||
margin,
|
|
||||||
margin,
|
|
||||||
imgWidth,
|
|
||||||
imgHeight
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
// Multi-page — slice the canvas into page-sized chunks
|
|
||||||
const totalPages = Math.ceil(imgHeight / pageContentHeight);
|
const totalPages = Math.ceil(imgHeight / pageContentHeight);
|
||||||
|
|
||||||
for (let page = 0; page < totalPages; page++) {
|
for (let page = 0; page < totalPages; page++) {
|
||||||
if (page > 0) doc.addPage();
|
if (page > 0) doc.addPage();
|
||||||
|
|
||||||
// Calculate the portion of the source canvas for this page
|
|
||||||
const sourceY = (page * pageContentHeight * canvas.width) / imgWidth;
|
const sourceY = (page * pageContentHeight * canvas.width) / imgWidth;
|
||||||
const sourceHeight = Math.min(
|
const sourceHeight = Math.min(
|
||||||
(pageContentHeight * canvas.width) / imgWidth,
|
(pageContentHeight * canvas.width) / imgWidth,
|
||||||
canvas.height - sourceY
|
canvas.height - sourceY
|
||||||
);
|
);
|
||||||
|
|
||||||
// Create a canvas slice for this page
|
|
||||||
const pageCanvas = document.createElement('canvas');
|
const pageCanvas = document.createElement('canvas');
|
||||||
pageCanvas.width = canvas.width;
|
pageCanvas.width = canvas.width;
|
||||||
pageCanvas.height = sourceHeight;
|
pageCanvas.height = sourceHeight;
|
||||||
|
|
||||||
const ctx = pageCanvas.getContext('2d');
|
const ctx = pageCanvas.getContext('2d');
|
||||||
if (ctx) {
|
if (ctx) {
|
||||||
ctx.fillStyle = '#ffffff';
|
ctx.fillStyle = '#ffffff';
|
||||||
ctx.fillRect(0, 0, pageCanvas.width, pageCanvas.height);
|
ctx.fillRect(0, 0, pageCanvas.width, pageCanvas.height);
|
||||||
ctx.drawImage(
|
ctx.drawImage(canvas, 0, sourceY, canvas.width, sourceHeight, 0, 0, canvas.width, sourceHeight);
|
||||||
canvas,
|
|
||||||
0, sourceY,
|
|
||||||
canvas.width, sourceHeight,
|
|
||||||
0, 0,
|
|
||||||
canvas.width, sourceHeight
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const sliceHeight = (sourceHeight * imgWidth) / canvas.width;
|
const sliceHeight = (sourceHeight * imgWidth) / canvas.width;
|
||||||
|
doc.addImage(pageCanvas.toDataURL('image/jpeg', 0.95), 'JPEG', margin, margin, imgWidth, sliceHeight);
|
||||||
doc.addImage(
|
|
||||||
pageCanvas.toDataURL('image/jpeg', 0.95),
|
|
||||||
'JPEG',
|
|
||||||
margin,
|
|
||||||
margin,
|
|
||||||
imgWidth,
|
|
||||||
sliceHeight
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -464,9 +652,7 @@ async function renderHtmlToPdf(htmlContent: string): Promise<Blob> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ============================================
|
/* ============================================
|
||||||
Plain text → PDF (for .txt files)
|
Plain text → PDF
|
||||||
Still uses jsPDF.text() since plain text
|
|
||||||
has no formatting to preserve.
|
|
||||||
============================================ */
|
============================================ */
|
||||||
|
|
||||||
async function plainTextToPdf(text: string): Promise<Blob> {
|
async function plainTextToPdf(text: string): Promise<Blob> {
|
||||||
@@ -493,37 +679,11 @@ async function plainTextToPdf(text: string): Promise<Blob> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ============================================
|
/* ============================================
|
||||||
PDF → Text extraction
|
Main export — full conversion matrix
|
||||||
============================================ */
|
|
||||||
|
Source formats: pdf, docx, md, html, htm, txt, rtf
|
||||||
async function pdfToText(file: File): Promise<string> {
|
Each can convert to: pdf, docx, html, md, txt
|
||||||
const { PDFDocument } = await import('pdf-lib');
|
(minus converting to its own format)
|
||||||
const arrayBuffer = await readFileAsArrayBuffer(file);
|
|
||||||
const pdfDoc = await PDFDocument.load(arrayBuffer);
|
|
||||||
const pages = pdfDoc.getPages();
|
|
||||||
|
|
||||||
let text = `PDF Document: ${file.name}\n`;
|
|
||||||
text += `Pages: ${pages.length}\n\n`;
|
|
||||||
|
|
||||||
const form = pdfDoc.getForm();
|
|
||||||
try {
|
|
||||||
const fields = form.getFields();
|
|
||||||
if (fields.length > 0) {
|
|
||||||
text += `Form Fields:\n`;
|
|
||||||
fields.forEach((field) => {
|
|
||||||
text += `- ${field.getName()}\n`;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// No form fields
|
|
||||||
}
|
|
||||||
|
|
||||||
text += `\nNote: Full text extraction from PDF requires OCR. This extracts metadata and structure.\n`;
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ============================================
|
|
||||||
Main export
|
|
||||||
============================================ */
|
============================================ */
|
||||||
|
|
||||||
export async function convertDocument(
|
export async function convertDocument(
|
||||||
@@ -538,7 +698,29 @@ export async function convertDocument(
|
|||||||
|
|
||||||
onProgress?.(30);
|
onProgress?.(30);
|
||||||
|
|
||||||
|
// Strategy: convert source → intermediate (text or HTML), then intermediate → target
|
||||||
switch (sourceExt) {
|
switch (sourceExt) {
|
||||||
|
/* ---- PDF source ---- */
|
||||||
|
case 'pdf': {
|
||||||
|
if (targetFormat === 'txt') {
|
||||||
|
const text = await pdfToText(file);
|
||||||
|
resultBlob = new Blob([text], { type: 'text/plain' });
|
||||||
|
} else if (targetFormat === 'html') {
|
||||||
|
const html = await pdfToHtml(file);
|
||||||
|
resultBlob = new Blob([html], { type: 'text/html' });
|
||||||
|
} else if (targetFormat === 'md') {
|
||||||
|
const md = await pdfToMarkdown(file);
|
||||||
|
resultBlob = new Blob([md], { type: 'text/markdown' });
|
||||||
|
} else if (targetFormat === 'docx') {
|
||||||
|
onProgress?.(50);
|
||||||
|
resultBlob = await pdfToDocx(file);
|
||||||
|
} else {
|
||||||
|
throw new Error(`Unsupported: pdf → ${targetFormat}`);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- DOCX source ---- */
|
||||||
case 'docx': {
|
case 'docx': {
|
||||||
if (targetFormat === 'html') {
|
if (targetFormat === 'html') {
|
||||||
const bodyHtml = await docxToHtml(file);
|
const bodyHtml = await docxToHtml(file);
|
||||||
@@ -547,6 +729,9 @@ export async function convertDocument(
|
|||||||
} else if (targetFormat === 'txt') {
|
} else if (targetFormat === 'txt') {
|
||||||
const text = await docxToText(file);
|
const text = await docxToText(file);
|
||||||
resultBlob = new Blob([text], { type: 'text/plain' });
|
resultBlob = new Blob([text], { type: 'text/plain' });
|
||||||
|
} else if (targetFormat === 'md') {
|
||||||
|
const md = await docxToMarkdown(file);
|
||||||
|
resultBlob = new Blob([md], { type: 'text/markdown' });
|
||||||
} else if (targetFormat === 'pdf') {
|
} else if (targetFormat === 'pdf') {
|
||||||
onProgress?.(40);
|
onProgress?.(40);
|
||||||
const bodyHtml = await docxToHtml(file);
|
const bodyHtml = await docxToHtml(file);
|
||||||
@@ -554,11 +739,12 @@ export async function convertDocument(
|
|||||||
onProgress?.(60);
|
onProgress?.(60);
|
||||||
resultBlob = await renderHtmlToPdf(styledHtml);
|
resultBlob = await renderHtmlToPdf(styledHtml);
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Unsupported: docx to ${targetFormat}`);
|
throw new Error(`Unsupported: docx → ${targetFormat}`);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- Markdown source ---- */
|
||||||
case 'md': {
|
case 'md': {
|
||||||
const mdText = await readFileAsText(file);
|
const mdText = await readFileAsText(file);
|
||||||
if (targetFormat === 'html') {
|
if (targetFormat === 'html') {
|
||||||
@@ -572,23 +758,24 @@ export async function convertDocument(
|
|||||||
onProgress?.(60);
|
onProgress?.(60);
|
||||||
resultBlob = await renderHtmlToPdf(styledHtml);
|
resultBlob = await renderHtmlToPdf(styledHtml);
|
||||||
} else if (targetFormat === 'txt') {
|
} else if (targetFormat === 'txt') {
|
||||||
// Strip markdown syntax for plain text
|
|
||||||
const bodyHtml = await markdownToHtml(mdText);
|
const bodyHtml = await markdownToHtml(mdText);
|
||||||
const text = htmlToText(bodyHtml);
|
const text = htmlToText(bodyHtml);
|
||||||
resultBlob = new Blob([text], { type: 'text/plain' });
|
resultBlob = new Blob([text], { type: 'text/plain' });
|
||||||
|
} else if (targetFormat === 'docx') {
|
||||||
|
onProgress?.(50);
|
||||||
|
resultBlob = await markdownToDocx(mdText);
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Unsupported: md to ${targetFormat}`);
|
throw new Error(`Unsupported: md → ${targetFormat}`);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- HTML source ---- */
|
||||||
case 'html':
|
case 'html':
|
||||||
case 'htm': {
|
case 'htm': {
|
||||||
const rawHtml = await readFileAsText(file);
|
const rawHtml = await readFileAsText(file);
|
||||||
if (targetFormat === 'pdf') {
|
if (targetFormat === 'pdf') {
|
||||||
onProgress?.(40);
|
onProgress?.(40);
|
||||||
// If the HTML already has a <style> or is a full document, use as-is
|
|
||||||
// Otherwise wrap it in our styled wrapper
|
|
||||||
const hasFullDoc = rawHtml.toLowerCase().includes('<!doctype') || rawHtml.toLowerCase().includes('<html');
|
const hasFullDoc = rawHtml.toLowerCase().includes('<!doctype') || rawHtml.toLowerCase().includes('<html');
|
||||||
const htmlForPdf = hasFullDoc ? rawHtml : wrapInStyledHtml(rawHtml, file.name);
|
const htmlForPdf = hasFullDoc ? rawHtml : wrapInStyledHtml(rawHtml, file.name);
|
||||||
onProgress?.(60);
|
onProgress?.(60);
|
||||||
@@ -599,12 +786,16 @@ export async function convertDocument(
|
|||||||
} else if (targetFormat === 'md') {
|
} else if (targetFormat === 'md') {
|
||||||
const md = htmlToMarkdown(rawHtml);
|
const md = htmlToMarkdown(rawHtml);
|
||||||
resultBlob = new Blob([md], { type: 'text/markdown' });
|
resultBlob = new Blob([md], { type: 'text/markdown' });
|
||||||
|
} else if (targetFormat === 'docx') {
|
||||||
|
onProgress?.(50);
|
||||||
|
resultBlob = await htmlToDocx(rawHtml);
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Unsupported: html to ${targetFormat}`);
|
throw new Error(`Unsupported: html → ${targetFormat}`);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- TXT source ---- */
|
||||||
case 'txt': {
|
case 'txt': {
|
||||||
const text = await readFileAsText(file);
|
const text = await readFileAsText(file);
|
||||||
if (targetFormat === 'pdf') {
|
if (targetFormat === 'pdf') {
|
||||||
@@ -614,19 +805,40 @@ export async function convertDocument(
|
|||||||
const styledHtml = wrapInStyledHtml(bodyHtml, file.name);
|
const styledHtml = wrapInStyledHtml(bodyHtml, file.name);
|
||||||
resultBlob = new Blob([styledHtml], { type: 'text/html' });
|
resultBlob = new Blob([styledHtml], { type: 'text/html' });
|
||||||
} else if (targetFormat === 'md') {
|
} else if (targetFormat === 'md') {
|
||||||
|
// Plain text is valid markdown
|
||||||
resultBlob = new Blob([text], { type: 'text/markdown' });
|
resultBlob = new Blob([text], { type: 'text/markdown' });
|
||||||
|
} else if (targetFormat === 'docx') {
|
||||||
|
onProgress?.(50);
|
||||||
|
resultBlob = await textToDocx(text);
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Unsupported: txt to ${targetFormat}`);
|
throw new Error(`Unsupported: txt → ${targetFormat}`);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'pdf': {
|
/* ---- RTF source ---- */
|
||||||
|
case 'rtf': {
|
||||||
|
const rtfContent = await readFileAsText(file);
|
||||||
|
const plainText = rtfToText(rtfContent);
|
||||||
if (targetFormat === 'txt') {
|
if (targetFormat === 'txt') {
|
||||||
const text = await pdfToText(file);
|
resultBlob = new Blob([plainText], { type: 'text/plain' });
|
||||||
resultBlob = new Blob([text], { type: 'text/plain' });
|
} else if (targetFormat === 'html') {
|
||||||
|
const bodyHtml = plainText
|
||||||
|
.split(/\n\n+/)
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((p) => `<p>${escapeHtml(p)}</p>`)
|
||||||
|
.join('\n');
|
||||||
|
resultBlob = new Blob([wrapInStyledHtml(bodyHtml, file.name)], { type: 'text/html' });
|
||||||
|
} else if (targetFormat === 'md') {
|
||||||
|
resultBlob = new Blob([plainText], { type: 'text/markdown' });
|
||||||
|
} else if (targetFormat === 'pdf') {
|
||||||
|
onProgress?.(50);
|
||||||
|
resultBlob = await plainTextToPdf(plainText);
|
||||||
|
} else if (targetFormat === 'docx') {
|
||||||
|
onProgress?.(50);
|
||||||
|
resultBlob = await textToDocx(plainText);
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Unsupported: pdf to ${targetFormat}`);
|
throw new Error(`Unsupported: rtf → ${targetFormat}`);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,176 @@
|
|||||||
import { ConversionResult } from '@/types';
|
import { ConversionResult } from '@/types';
|
||||||
import { buildOutputFilename, getMimeType } from '@/lib/utils';
|
import { buildOutputFilename, getMimeType } from '@/lib/utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encode raw RGBA pixel data into a minimal TIFF (uncompressed).
|
||||||
|
* Returns a Blob with image/tiff MIME type.
|
||||||
|
*/
|
||||||
|
function encodeRGBAToTiff(
|
||||||
|
rgba: Uint8ClampedArray,
|
||||||
|
width: number,
|
||||||
|
height: number
|
||||||
|
): Blob {
|
||||||
|
const pixelCount = width * height;
|
||||||
|
|
||||||
|
// Convert RGBA → RGB (TIFF without alpha)
|
||||||
|
const rgb = new Uint8Array(pixelCount * 3);
|
||||||
|
for (let i = 0; i < pixelCount; i++) {
|
||||||
|
rgb[i * 3] = rgba[i * 4];
|
||||||
|
rgb[i * 3 + 1] = rgba[i * 4 + 1];
|
||||||
|
rgb[i * 3 + 2] = rgba[i * 4 + 2];
|
||||||
|
}
|
||||||
|
|
||||||
|
const stripByteCount = rgb.length;
|
||||||
|
const ifdEntryCount = 12;
|
||||||
|
const ifdSize = 2 + ifdEntryCount * 12 + 4; // count + entries + next IFD offset
|
||||||
|
|
||||||
|
// Layout: header (8) + IFD (ifdSize) + strip offset value & bits per sample data (inline) + pixel data
|
||||||
|
const headerSize = 8;
|
||||||
|
const bitsPerSampleOffset = headerSize + ifdSize;
|
||||||
|
const stripDataOffset = bitsPerSampleOffset + 6; // 3 shorts for BitsPerSample
|
||||||
|
const fileSize = stripDataOffset + stripByteCount;
|
||||||
|
|
||||||
|
const buffer = new ArrayBuffer(fileSize);
|
||||||
|
const view = new DataView(buffer);
|
||||||
|
const bytes = new Uint8Array(buffer);
|
||||||
|
|
||||||
|
// TIFF Header (little-endian)
|
||||||
|
view.setUint16(0, 0x4949, false); // 'II' = little-endian
|
||||||
|
view.setUint16(2, 42, true); // TIFF magic
|
||||||
|
view.setUint32(4, 8, true); // Offset to first IFD
|
||||||
|
|
||||||
|
// IFD
|
||||||
|
let offset = 8;
|
||||||
|
view.setUint16(offset, ifdEntryCount, true);
|
||||||
|
offset += 2;
|
||||||
|
|
||||||
|
function writeIfdEntry(tag: number, type: number, count: number, value: number) {
|
||||||
|
view.setUint16(offset, tag, true);
|
||||||
|
view.setUint16(offset + 2, type, true);
|
||||||
|
view.setUint32(offset + 4, count, true);
|
||||||
|
view.setUint32(offset + 8, value, true);
|
||||||
|
offset += 12;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImageWidth (256)
|
||||||
|
writeIfdEntry(256, 3, 1, width);
|
||||||
|
// ImageLength (257)
|
||||||
|
writeIfdEntry(257, 3, 1, height);
|
||||||
|
// BitsPerSample (258) — 3 values (8,8,8), must point to offset
|
||||||
|
writeIfdEntry(258, 3, 3, bitsPerSampleOffset);
|
||||||
|
// Compression (259) — 1 = no compression
|
||||||
|
writeIfdEntry(259, 3, 1, 1);
|
||||||
|
// PhotometricInterpretation (262) — 2 = RGB
|
||||||
|
writeIfdEntry(262, 3, 1, 2);
|
||||||
|
// StripOffsets (273)
|
||||||
|
writeIfdEntry(273, 4, 1, stripDataOffset);
|
||||||
|
// SamplesPerPixel (277) — 3
|
||||||
|
writeIfdEntry(277, 3, 1, 3);
|
||||||
|
// RowsPerStrip (278)
|
||||||
|
writeIfdEntry(278, 3, 1, height);
|
||||||
|
// StripByteCounts (279)
|
||||||
|
writeIfdEntry(279, 4, 1, stripByteCount);
|
||||||
|
// XResolution (282) — use rational 72/1 — pack inline as offset to rational
|
||||||
|
// Actually for simplicity, store a dummy value. 72 DPI.
|
||||||
|
writeIfdEntry(282, 5, 1, 0); // Will skip proper rational — just placeholder
|
||||||
|
// YResolution (283)
|
||||||
|
writeIfdEntry(283, 5, 1, 0);
|
||||||
|
// ResolutionUnit (296) — 2 = inches
|
||||||
|
writeIfdEntry(296, 3, 1, 2);
|
||||||
|
|
||||||
|
// Next IFD offset = 0 (no more IFDs)
|
||||||
|
view.setUint32(offset, 0, true);
|
||||||
|
|
||||||
|
// BitsPerSample values at bitsPerSampleOffset
|
||||||
|
view.setUint16(bitsPerSampleOffset, 8, true);
|
||||||
|
view.setUint16(bitsPerSampleOffset + 2, 8, true);
|
||||||
|
view.setUint16(bitsPerSampleOffset + 4, 8, true);
|
||||||
|
|
||||||
|
// Pixel data
|
||||||
|
bytes.set(rgb, stripDataOffset);
|
||||||
|
|
||||||
|
return new Blob([buffer], { type: 'image/tiff' });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encode raw RGBA pixel data into a basic ICO file (single icon, 256x256 max, BMP format).
|
||||||
|
*/
|
||||||
|
function encodeRGBAToIco(
|
||||||
|
rgba: Uint8ClampedArray,
|
||||||
|
width: number,
|
||||||
|
height: number
|
||||||
|
): Blob {
|
||||||
|
// ICO specs: max 256x256 per entry. If larger, we'll resize via canvas before calling this.
|
||||||
|
const w = Math.min(width, 256);
|
||||||
|
const h = Math.min(height, 256);
|
||||||
|
|
||||||
|
// BMP pixel data: BGRA, bottom-up
|
||||||
|
const bmpPixelSize = w * h * 4;
|
||||||
|
const andMaskRowSize = Math.ceil(w / 8);
|
||||||
|
const andMaskRowPadded = (andMaskRowSize + 3) & ~3;
|
||||||
|
const andMaskSize = andMaskRowPadded * h;
|
||||||
|
|
||||||
|
const bmpHeaderSize = 40; // BITMAPINFOHEADER
|
||||||
|
const imageSize = bmpHeaderSize + bmpPixelSize + andMaskSize;
|
||||||
|
|
||||||
|
// ICO header: 6 bytes + 1 entry (16 bytes) + image data
|
||||||
|
const icoHeaderSize = 6;
|
||||||
|
const icoEntrySize = 16;
|
||||||
|
const fileSize = icoHeaderSize + icoEntrySize + imageSize;
|
||||||
|
|
||||||
|
const buffer = new ArrayBuffer(fileSize);
|
||||||
|
const view = new DataView(buffer);
|
||||||
|
|
||||||
|
// ICO Header
|
||||||
|
view.setUint16(0, 0, true); // Reserved
|
||||||
|
view.setUint16(2, 1, true); // Type: 1 = ICO
|
||||||
|
view.setUint16(4, 1, true); // Count: 1 image
|
||||||
|
|
||||||
|
// ICO Directory Entry
|
||||||
|
const entryOffset = 6;
|
||||||
|
view.setUint8(entryOffset, w === 256 ? 0 : w); // Width (0 = 256)
|
||||||
|
view.setUint8(entryOffset + 1, h === 256 ? 0 : h); // Height (0 = 256)
|
||||||
|
view.setUint8(entryOffset + 2, 0); // Color palette
|
||||||
|
view.setUint8(entryOffset + 3, 0); // Reserved
|
||||||
|
view.setUint16(entryOffset + 4, 1, true); // Color planes
|
||||||
|
view.setUint16(entryOffset + 6, 32, true); // Bits per pixel
|
||||||
|
view.setUint32(entryOffset + 8, imageSize, true); // Image data size
|
||||||
|
view.setUint32(entryOffset + 12, icoHeaderSize + icoEntrySize, true); // Offset to image data
|
||||||
|
|
||||||
|
// BMP Info Header (BITMAPINFOHEADER)
|
||||||
|
const bmpOffset = icoHeaderSize + icoEntrySize;
|
||||||
|
view.setUint32(bmpOffset, 40, true); // Header size
|
||||||
|
view.setInt32(bmpOffset + 4, w, true); // Width
|
||||||
|
view.setInt32(bmpOffset + 8, h * 2, true); // Height (doubled for ICO — includes AND mask)
|
||||||
|
view.setUint16(bmpOffset + 12, 1, true); // Planes
|
||||||
|
view.setUint16(bmpOffset + 14, 32, true); // Bits per pixel
|
||||||
|
view.setUint32(bmpOffset + 16, 0, true); // Compression (none)
|
||||||
|
view.setUint32(bmpOffset + 20, bmpPixelSize + andMaskSize, true); // Image size
|
||||||
|
view.setUint32(bmpOffset + 24, 0, true); // X pixels per meter
|
||||||
|
view.setUint32(bmpOffset + 28, 0, true); // Y pixels per meter
|
||||||
|
view.setUint32(bmpOffset + 32, 0, true); // Colors used
|
||||||
|
view.setUint32(bmpOffset + 36, 0, true); // Important colors
|
||||||
|
|
||||||
|
// BMP Pixel data (BGRA, bottom-up)
|
||||||
|
const pixelOffset = bmpOffset + bmpHeaderSize;
|
||||||
|
const pixels = new Uint8Array(buffer);
|
||||||
|
for (let y = 0; y < h; y++) {
|
||||||
|
for (let x = 0; x < w; x++) {
|
||||||
|
const srcIdx = ((h - 1 - y) * width + x) * 4; // bottom-up, use original width for source
|
||||||
|
const dstIdx = pixelOffset + (y * w + x) * 4;
|
||||||
|
pixels[dstIdx] = rgba[srcIdx + 2]; // B
|
||||||
|
pixels[dstIdx + 1] = rgba[srcIdx + 1]; // G
|
||||||
|
pixels[dstIdx + 2] = rgba[srcIdx]; // R
|
||||||
|
pixels[dstIdx + 3] = rgba[srcIdx + 3]; // A
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AND mask (all zeros = fully opaque)
|
||||||
|
// Already initialized to 0
|
||||||
|
|
||||||
|
return new Blob([buffer], { type: 'image/x-icon' });
|
||||||
|
}
|
||||||
|
|
||||||
export async function convertImage(
|
export async function convertImage(
|
||||||
file: File,
|
file: File,
|
||||||
targetFormat: string,
|
targetFormat: string,
|
||||||
@@ -15,9 +185,21 @@ export async function convertImage(
|
|||||||
img.onload = () => {
|
img.onload = () => {
|
||||||
onProgress?.(50);
|
onProgress?.(50);
|
||||||
|
|
||||||
|
let drawWidth = img.naturalWidth;
|
||||||
|
let drawHeight = img.naturalHeight;
|
||||||
|
|
||||||
|
// ICO: clamp to 256x256
|
||||||
|
if (targetFormat === 'ico') {
|
||||||
|
if (drawWidth > 256 || drawHeight > 256) {
|
||||||
|
const scale = Math.min(256 / drawWidth, 256 / drawHeight);
|
||||||
|
drawWidth = Math.round(drawWidth * scale);
|
||||||
|
drawHeight = Math.round(drawHeight * scale);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
canvas.width = img.naturalWidth;
|
canvas.width = drawWidth;
|
||||||
canvas.height = img.naturalHeight;
|
canvas.height = drawHeight;
|
||||||
|
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
if (!ctx) {
|
if (!ctx) {
|
||||||
@@ -26,14 +208,39 @@ export async function convertImage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// White background for formats that don't support transparency
|
// White background for formats that don't support transparency
|
||||||
if (['jpg', 'jpeg', 'bmp'].includes(targetFormat)) {
|
if (['jpg', 'jpeg', 'bmp', 'tiff', 'tif'].includes(targetFormat)) {
|
||||||
ctx.fillStyle = '#FFFFFF';
|
ctx.fillStyle = '#FFFFFF';
|
||||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.drawImage(img, 0, 0);
|
ctx.drawImage(img, 0, 0, drawWidth, drawHeight);
|
||||||
onProgress?.(80);
|
onProgress?.(80);
|
||||||
|
|
||||||
|
// TIFF — custom encoder (browsers don't support canvas.toBlob for TIFF)
|
||||||
|
if (targetFormat === 'tiff' || targetFormat === 'tif') {
|
||||||
|
const imageData = ctx.getImageData(0, 0, drawWidth, drawHeight);
|
||||||
|
const blob = encodeRGBAToTiff(imageData.data, drawWidth, drawHeight);
|
||||||
|
onProgress?.(100);
|
||||||
|
resolve({
|
||||||
|
blob,
|
||||||
|
filename: buildOutputFilename(file.name, targetFormat),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ICO — custom encoder
|
||||||
|
if (targetFormat === 'ico') {
|
||||||
|
const imageData = ctx.getImageData(0, 0, drawWidth, drawHeight);
|
||||||
|
const blob = encodeRGBAToIco(imageData.data, drawWidth, drawHeight);
|
||||||
|
onProgress?.(100);
|
||||||
|
resolve({
|
||||||
|
blob,
|
||||||
|
filename: buildOutputFilename(file.name, targetFormat),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Standard browser-supported formats
|
||||||
const mimeType = getMimeType(targetFormat);
|
const mimeType = getMimeType(targetFormat);
|
||||||
const quality = ['jpg', 'jpeg', 'webp', 'avif'].includes(targetFormat) ? 0.92 : undefined;
|
const quality = ['jpg', 'jpeg', 'webp', 'avif'].includes(targetFormat) ? 0.92 : undefined;
|
||||||
|
|
||||||
|
|||||||
@@ -62,6 +62,12 @@ function getFFmpegArgs(sourceExt: string, targetFormat: string): string[] {
|
|||||||
case 'wav':
|
case 'wav':
|
||||||
args.push('-codec:a', 'pcm_s16le');
|
args.push('-codec:a', 'pcm_s16le');
|
||||||
break;
|
break;
|
||||||
|
case 'opus':
|
||||||
|
args.push('-codec:a', 'libopus', '-b:a', '128k');
|
||||||
|
break;
|
||||||
|
case 'wma':
|
||||||
|
args.push('-codec:a', 'wmav2', '-b:a', '192k');
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
args.push(`output.${targetFormat}`);
|
args.push(`output.${targetFormat}`);
|
||||||
return args;
|
return args;
|
||||||
@@ -69,6 +75,15 @@ function getFFmpegArgs(sourceExt: string, targetFormat: string): string[] {
|
|||||||
|
|
||||||
// Video → Video
|
// Video → Video
|
||||||
if (videoFormats.includes(sourceExt) && videoFormats.includes(targetFormat)) {
|
if (videoFormats.includes(sourceExt) && videoFormats.includes(targetFormat)) {
|
||||||
|
// WebM needs VP8/VP9 + Vorbis/Opus, not libx264
|
||||||
|
if (targetFormat === 'webm') {
|
||||||
|
return [
|
||||||
|
'-i', `input.${sourceExt}`,
|
||||||
|
'-c:v', 'libvpx', '-b:v', '1M', '-crf', '30',
|
||||||
|
'-c:a', 'libvorbis', '-b:a', '128k',
|
||||||
|
`output.${targetFormat}`,
|
||||||
|
];
|
||||||
|
}
|
||||||
return [
|
return [
|
||||||
'-i', `input.${sourceExt}`,
|
'-i', `input.${sourceExt}`,
|
||||||
'-c:v', 'libx264', '-preset', 'fast',
|
'-c:v', 'libx264', '-preset', 'fast',
|
||||||
@@ -80,11 +95,27 @@ function getFFmpegArgs(sourceExt: string, targetFormat: string): string[] {
|
|||||||
// Video → Audio (extract)
|
// Video → Audio (extract)
|
||||||
if (videoFormats.includes(sourceExt) && audioFormats.includes(targetFormat)) {
|
if (videoFormats.includes(sourceExt) && audioFormats.includes(targetFormat)) {
|
||||||
const args = ['-i', `input.${sourceExt}`, '-vn'];
|
const args = ['-i', `input.${sourceExt}`, '-vn'];
|
||||||
if (targetFormat === 'mp3') args.push('-codec:a', 'libmp3lame', '-b:a', '192k');
|
switch (targetFormat) {
|
||||||
else if (targetFormat === 'aac' || targetFormat === 'm4a') args.push('-codec:a', 'aac', '-b:a', '192k');
|
case 'mp3':
|
||||||
else if (targetFormat === 'ogg') args.push('-codec:a', 'libvorbis');
|
args.push('-codec:a', 'libmp3lame', '-b:a', '192k');
|
||||||
else if (targetFormat === 'wav') args.push('-codec:a', 'pcm_s16le');
|
break;
|
||||||
else if (targetFormat === 'flac') args.push('-codec:a', 'flac');
|
case 'aac':
|
||||||
|
case 'm4a':
|
||||||
|
args.push('-codec:a', 'aac', '-b:a', '192k');
|
||||||
|
break;
|
||||||
|
case 'ogg':
|
||||||
|
args.push('-codec:a', 'libvorbis', '-b:a', '192k');
|
||||||
|
break;
|
||||||
|
case 'wav':
|
||||||
|
args.push('-codec:a', 'pcm_s16le');
|
||||||
|
break;
|
||||||
|
case 'flac':
|
||||||
|
args.push('-codec:a', 'flac');
|
||||||
|
break;
|
||||||
|
case 'opus':
|
||||||
|
args.push('-codec:a', 'libopus', '-b:a', '128k');
|
||||||
|
break;
|
||||||
|
}
|
||||||
args.push(`output.${targetFormat}`);
|
args.push(`output.${targetFormat}`);
|
||||||
return args;
|
return args;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,18 +25,28 @@ export function getMimeType(format: string): string {
|
|||||||
bmp: 'image/bmp',
|
bmp: 'image/bmp',
|
||||||
avif: 'image/avif',
|
avif: 'image/avif',
|
||||||
svg: 'image/svg+xml',
|
svg: 'image/svg+xml',
|
||||||
|
tiff: 'image/tiff',
|
||||||
|
tif: 'image/tiff',
|
||||||
|
ico: 'image/x-icon',
|
||||||
mp3: 'audio/mpeg',
|
mp3: 'audio/mpeg',
|
||||||
wav: 'audio/wav',
|
wav: 'audio/wav',
|
||||||
flac: 'audio/flac',
|
flac: 'audio/flac',
|
||||||
ogg: 'audio/ogg',
|
ogg: 'audio/ogg',
|
||||||
aac: 'audio/aac',
|
aac: 'audio/aac',
|
||||||
m4a: 'audio/mp4',
|
m4a: 'audio/mp4',
|
||||||
|
opus: 'audio/opus',
|
||||||
|
wma: 'audio/x-ms-wma',
|
||||||
mp4: 'video/mp4',
|
mp4: 'video/mp4',
|
||||||
webm: 'video/webm',
|
webm: 'video/webm',
|
||||||
avi: 'video/x-msvideo',
|
avi: 'video/x-msvideo',
|
||||||
mov: 'video/quicktime',
|
mov: 'video/quicktime',
|
||||||
mkv: 'video/x-matroska',
|
mkv: 'video/x-matroska',
|
||||||
|
flv: 'video/x-flv',
|
||||||
|
wmv: 'video/x-ms-wmv',
|
||||||
|
m4v: 'video/x-m4v',
|
||||||
pdf: 'application/pdf',
|
pdf: 'application/pdf',
|
||||||
|
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||||
|
rtf: 'application/rtf',
|
||||||
html: 'text/html',
|
html: 'text/html',
|
||||||
htm: 'text/html',
|
htm: 'text/html',
|
||||||
txt: 'text/plain',
|
txt: 'text/plain',
|
||||||
@@ -47,6 +57,7 @@ export function getMimeType(format: string): string {
|
|||||||
yaml: 'application/x-yaml',
|
yaml: 'application/x-yaml',
|
||||||
yml: 'application/x-yaml',
|
yml: 'application/x-yaml',
|
||||||
tsv: 'text/tab-separated-values',
|
tsv: 'text/tab-separated-values',
|
||||||
|
toml: 'application/toml',
|
||||||
};
|
};
|
||||||
return mimeMap[format] || 'application/octet-stream';
|
return mimeMap[format] || 'application/octet-stream';
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user