diff --git a/README.md b/README.md index 9465f064..fa5714d7 100644 --- a/README.md +++ b/README.md @@ -88,66 +88,136 @@ If you are interested in learning about this, take a look at the Example workflo Current functions of spdf and their progress in this repo. +#### Page Operations | Status | Feature | Description | | ------ | ------------------------ | ----------- | -| ✔️ | Merge | | -| ✔️ | Split | | -| ✔️ | Rotate | | -| ✔️ | Multi-Page-Layout | | -| ✔️ | Adjust page size/scale | | -| ✔️ | Organize | | -| ✔️ | Change Metadata | | -| ✔️ | Auto Rename | | -| ❌ | Add Watermark | | +| 🚧A | Merge | | +| 🚧A | Split | | +| 🚧A | Organize | | +| 🚧S | Rotate | | +| 🚧A | Remove Pages | | +| 🚧A | Multi-Page Layout | | +| ❌ | Adjust page size/scale | | +| 🚧A | Auto Split Pages | | +| ❌ | Adjust Colours/Contrast | | +| ❌ | Crop | | +| 🚧A | Extract Pages | | | ❌ | PDF to Single large Page | | -| ❌ | Auto Redact | | -| ❌ | Remove Pages | | -| Status | Feature | Description | -| ------ | ------------------ | ----------- | -| ✔️ | Remove Blank Pages | | -| ✔️ | Auto Split Pages | | -| Status | Feature | Description | -| ------ | ------------ | ----------- | -| ❌ | Repair | | -| ❌ | Compress | | -| ❌ | Flatten | | -| ❌ | Compare/Diff | | -| ❌ | Sanitize | | -| ❌ | Get info | | -| ❌ | Show JS | | +#### Convert +| Status | Feature | Description | +| ------ | ------------------- | ----------- | +| ❌ | Image to PDF | | +| 🚧S | Convert file to PDF | | +| ❌ | URL to PDF | | +| ❌ | HTML to PDF | | +| ❌ | Markdown to PDF | | +| ❌ | PDF to Image | | +| ❌ | PDF to Word | | +| ❌ | PDF to Presentation | | +| ❌ | PDF to Text/RTF | | +| ❌ | PDF to HTML | | +| ❌ | PDF to PDF/A | | +#### Security | Status | Feature | Description | | ------ | --------------------- | ----------- | -| ❌ | Sign | | -| ❌ | Sign with Certificate | | | ❌ | Add Password | | | ❌ | Remove Password | | | ❌ | Change Permissions | | +| ❌ | Add Watermark | | +| ❌ | Sign with Certificate | | +| ❌ | Sanitize | | +| ❌ | Auto Redact | | +#### Miscellaneous | Status | Feature | Description | | ------ | --------------------------- | ----------- | -| ❌ | Image to PDF | | -| ❌ | Add image | | -| ❌ | Extract Images | | -| ❌ | PDF to Image | | | ❌ | OCR | | -| ❌ | Detect/Split Scanned photos | | +| ❌ | Add image | | +| ❌ | Compress | | +| ❌ | Extract Images | | +| 🚧S | Change Metadata | | +| 🚧A | Detect/Split Scanned photos | | +| ❌ | Sign | | +| ❌ | Flatten | | +| ❌ | Repair | | +| 🚧A | Remove Blank Pages | | +| ❌ | Compare/Diff | | +| ❌ | Add Page Numbers | | +| ❌ | Auto Rename | | +| ❌ | Get info | | +| ❌ | Show JS | | + -| Status | Feature | Description | -| ------ | ------------------------ | ----------- | -| ❌ | Convert file to PDF | | -| ❌ | PDF to Text/RTF | | -| ❌ | PDF to HTML | | -| ❌ | PDF to XML | | -| ❌ | URL to PDF | | -| ❌ | HTML to PDF | | -| ❌ | Markdown to PDF | | ✔️: Done, 🚧: Started Developement, ❌: Planned Feature +A: Available in the internal API, S: Available on the node server, C: Available in the client ## Contribute For initial instructions look at [CONTRIBUTE.md](./CONTRIBUTE.md) + + + +/* +///// CONVERT 2 pdf +file2pdf +url2pdf +html2pdf +md2pdf +image2pdf + +///// CONVERT from pdf +pdf2image +flatten +pdf2pdf/a +pdf2word +pdf2presentation +pdf2rtf +pdf2html +pdf2xml + +///// SINGLE +merge +rotate +crop +pageNumbers +colours/contrast +addPassword +removePassword +compress +changeMetadata +change Permissions +OCR +sanitise +repair +compare +extract images +signWith certificate +impose +adjust page size/scale +auto rename +getAllInfo +showJS +redact +pdf2singleLargePage + +///// SPLITTING +split +auto split +detect/split scanned + +///// REARRANGE +- organise pages (remove/re-arrange) +- removePages +- removeBlank +- extractPages + +///// ADD OBJECTS +add image +add watermark +sign +*/ \ No newline at end of file diff --git a/client-tauri/package.json b/client-tauri/package.json index 466785cd..04a0fd96 100644 --- a/client-tauri/package.json +++ b/client-tauri/package.json @@ -17,13 +17,15 @@ "i18next": "^23.6.0", "i18next-browser-languagedetector": "^7.1.0", "path-browserify": "^1.0.1", + "pdfjs-dist": "^4.0.189", "react": "^18.2.0", "react-bootstrap": "^2.9.1", "react-dom": "^18.2.0", "react-i18next": "^13.3.1", "react-icons": "^4.11.0", "react-router-bootstrap": "^0.26.2", - "react-router-dom": "^6.18.0" + "react-router-dom": "^6.18.0", + "vite-plugin-top-level-await": "^1.3.1" }, "devDependencies": { "@tauri-apps/cli": "^1.5.0", diff --git a/client-tauri/src-tauri/Cargo.lock b/client-tauri/src-tauri/Cargo.lock index 2eeb383d..31cd7ff3 100644 --- a/client-tauri/src-tauri/Cargo.lock +++ b/client-tauri/src-tauri/Cargo.lock @@ -1711,6 +1711,16 @@ dependencies = [ "windows-sys 0.42.0", ] +[[package]] +name = "os_pipe" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae859aa07428ca9a929b936690f8b12dc5f11dd8c6992a18ca93919f28bc177" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + [[package]] name = "overload" version = "0.1.1" @@ -2436,6 +2446,16 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shared_child" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0d94659ad3c2137fef23ae75b03d5241d633f8acded53d672decfa0e6e0caef" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "simd-adler32" version = "0.3.7" @@ -2693,6 +2713,7 @@ dependencies = [ "objc", "once_cell", "open", + "os_pipe", "percent-encoding", "rand 0.8.5", "raw-window-handle", @@ -2703,6 +2724,7 @@ dependencies = [ "serde_json", "serde_repr", "serialize-to-javascript", + "shared_child", "state", "tar", "tauri-macros", diff --git a/client-tauri/src-tauri/Cargo.toml b/client-tauri/src-tauri/Cargo.toml index d4a9a4e0..8c60e697 100644 --- a/client-tauri/src-tauri/Cargo.toml +++ b/client-tauri/src-tauri/Cargo.toml @@ -13,7 +13,7 @@ edition = "2021" tauri-build = { version = "1.5", features = [] } [dependencies] -tauri = { version = "1.5", features = [ "fs-write-file", "fs-read-file", "dialog-save", "dialog-open", "shell-open"] } +tauri = { version = "1.5", features = [ "fs-remove-dir", "fs-create-dir", "shell-all", "fs-write-file", "fs-read-file", "dialog-save", "dialog-open"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" diff --git a/client-tauri/src-tauri/tauri.conf.json b/client-tauri/src-tauri/tauri.conf.json index 2efa9aaf..23ada3f1 100644 --- a/client-tauri/src-tauri/tauri.conf.json +++ b/client-tauri/src-tauri/tauri.conf.json @@ -14,8 +14,19 @@ "allowlist": { "all": false, "shell": { - "all": false, - "open": true + "all": true, + "open": true, + "scope": [ + { + "name": "libreoffice-version", + "cmd": "libreoffice", + "args": ["--version"] + },{ + "name": "libreoffice-convert", + "cmd": "libreoffice", + "args": ["--headless","--convert-to",{ "validator": "\\S+" },{ "validator": "\\S+" },"--outdir",{ "validator": "\\S+" }] + } + ] }, "dialog": { "all": false, @@ -31,8 +42,8 @@ "writeFile": true, "readDir": false, "copyFile": false, - "createDir": false, - "removeDir": false, + "createDir": true, + "removeDir": true, "removeFile": false, "renameFile": false, "exists": false diff --git a/client-tauri/src/App.tsx b/client-tauri/src/App.tsx index fe7bda09..94bb6007 100644 --- a/client-tauri/src/App.tsx +++ b/client-tauri/src/App.tsx @@ -4,6 +4,7 @@ import { Routes, Route, Outlet } from "react-router-dom"; import Home from "./pages/Home"; import About from "./pages/About"; import Dashboard from "./pages/Dashboard"; +import ToPdf from "./pages/convert/ToPdf" import NoMatch from "./pages/NoMatch"; import NavBar from "./components/NavBar"; @@ -38,6 +39,7 @@ export default function App() { } /> } /> } /> + } /> {/* Using path="*"" means "match anything", so this route acts like a catch-all for URLs that we don't have explicit diff --git a/client-tauri/src/components/NavBar.tsx b/client-tauri/src/components/NavBar.tsx index 45978fe5..7073910c 100644 --- a/client-tauri/src/components/NavBar.tsx +++ b/client-tauri/src/components/NavBar.tsx @@ -86,7 +86,7 @@ function NavBar() { ]}, {displayText: t('navbar.convert'), icon: BsArrowLeftRight, sublist: [ { displayText: t('home.imageToPdf.title'), icon: BsFileEarmarkImage, dest: "/dashboard", tooltip: t('home.imageToPdf.desc') }, - { displayText: t('home.fileToPDF.title'), icon: BsFileEarmark, dest: "/nothing-here", tooltip: t('home.fileToPDF.desc') }, + { displayText: t('home.fileToPDF.title'), icon: BsFileEarmark, dest: "/to-pdf", tooltip: t('home.fileToPDF.desc') }, { displayText: t('home.HTMLToPDF.title'), icon: BsFiletypeHtml, dest: "/nothing-here", tooltip: t('home.HTMLToPDF.desc') }, { displayText: t('home.URLToPDF.title'), icon: BsLink, dest: "/nothing-here", tooltip: t('home.URLToPDF.desc') }, { displayText: t('home.MarkdownToPDF.title'), icon: BsFiletypeMd, dest: "/nothing-here", tooltip: t('home.MarkdownToPDF.desc') }, diff --git a/client-tauri/src/pages/convert/ToPdf.tsx b/client-tauri/src/pages/convert/ToPdf.tsx new file mode 100644 index 00000000..41d9e6f4 --- /dev/null +++ b/client-tauri/src/pages/convert/ToPdf.tsx @@ -0,0 +1,16 @@ + +import { isLibreOfficeInstalled } from "../../utils/libre-office-utils"; + +const hasLibreOffice = await isLibreOfficeInstalled(); +console.log(hasLibreOffice) + +function About() { + return ( +
+

Convert to PDF

+ {"hasLibreOffice: "+hasLibreOffice} +
+ ); +} + +export default About; diff --git a/client-tauri/src/utils/libre-office-utils.tsx b/client-tauri/src/utils/libre-office-utils.tsx new file mode 100644 index 00000000..47f5c432 --- /dev/null +++ b/client-tauri/src/utils/libre-office-utils.tsx @@ -0,0 +1,46 @@ + +import { readBinaryFile, writeBinaryFile, removeDir, BaseDirectory } from '@tauri-apps/api/fs'; +import { PdfFile, fromUint8Array } from '@stirling-pdf/shared-operations/wrappers/PdfFile' +import { runShell } from './tauri-wrapper'; + +export async function fileToPdf(byteArray: Uint8Array, filename: string): Promise { + const randUuid = crypto.randomUUID(); + const tempDir = "StirlingPDF/"+randUuid; + const srcFile = tempDir+"/"+filename; + + await writeBinaryFile(srcFile, byteArray); + await writeBinaryFile(srcFile, new Uint8Array([]), { dir: BaseDirectory.Temp }); + + const messageList: string[] = []; + await runShell("libreoffice-convert", ["--headless","--convert-to","pdf",srcFile,"--outdir",tempDir], (message, stream) => { + if (stream === "stdout") { + messageList.push(message); + } + console.debug(`${stream}, ${randUuid}: ${message}`); + }); + const lastMessage = messageList[messageList.length-1] + const outputFilePath = lastMessage.split(" -> ")[1].split(".pdf")[0]+".pdf"; + const outputFilePathSplit = outputFilePath.toString().split("[\\/]") + const outputFileName = outputFilePathSplit[outputFilePathSplit.length-1]; + const outputBytes = await readBinaryFile(outputFilePath); + + await removeDir(tempDir); + + return fromUint8Array(outputBytes, outputFileName); +} + +export async function isLibreOfficeInstalled() { + const messageList: string[] = []; + try { + await runShell("libreoffice-version", ["--version"], (message, stream) => { + if (stream === "stdout") { + messageList.push(message); + } + }); + } catch (error) { + return false; + } + console.log("messageList", messageList) + const result = messageList[0].match("LibreOffice ([0-9]+\.){4}.*"); + return result ? true : false; +} diff --git a/client-tauri/src/utils/tauri-wrapper.ts b/client-tauri/src/utils/tauri-wrapper.ts index 7e5aea84..3f71d837 100644 --- a/client-tauri/src/utils/tauri-wrapper.ts +++ b/client-tauri/src/utils/tauri-wrapper.ts @@ -1,6 +1,7 @@ import { open, save } from '@tauri-apps/api/dialog'; import { readBinaryFile, writeBinaryFile } from '@tauri-apps/api/fs'; +import { Command } from '@tauri-apps/api/shell' export type TauriBrowserFile = { name: string, @@ -52,7 +53,7 @@ export function openFiles(options: SelectFilesDialogOptions): Promise { if (input.files && input.files.length) { console.log("input.files", input.files) - const files:TauriBrowserFile[] = []; + const files: TauriBrowserFile[] = []; for (const f of input.files) { const contents = new Uint8Array(await f.arrayBuffer()); const res = byteArrayToFile(contents, f.name); @@ -138,4 +139,31 @@ export async function downloadFile(fileData: Uint8Array, options: DownloadFilesD downloadLink.click(); } } -} \ No newline at end of file +} + +/** + * Dont forget to whitelist the Command in src-tauri/tauri.conf.json! (tauri.allowlist.shell.scope) + * @param commandName The name of the command to run. Must be defined in tauri.allowlist.shell.scope[].name + * @param args The args to pass into the command + * @param callback A callback function that is called when output is logged + * @returns A log of all the outputs logged + */ +export function runShell(commandName: string, args: string[], callback: (message: any, stream:"stdout"|"stderr"|"error") => void): Promise { + return new Promise(async (resolve, reject) => { + + const comm = new Command(commandName, args); + comm.on('close', data => { + if (data.code === 0) { + resolve(); + } else { + reject(new Error(`Command failed with exit code ${data.code} and signal ${data.signal}`)); + } + }); + comm.on('error', error => callback(error, "error")); + comm.stdout.on('data', line => callback(line, "stdout")); + comm.stderr.on('data', line => callback(line, "stderr")); + + const child = await comm.spawn(); + console.debug(`Started child process with pid: ${child.pid}`) + }); +} diff --git a/client-tauri/vite.config.ts b/client-tauri/vite.config.ts index 833f3307..510ba1da 100644 --- a/client-tauri/vite.config.ts +++ b/client-tauri/vite.config.ts @@ -1,9 +1,18 @@ import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; +import topLevelAwait from "vite-plugin-top-level-await"; // https://vitejs.dev/config/ export default defineConfig(async () => ({ - plugins: [react()], + plugins: [ + react(), + topLevelAwait({ + // The export name of top-level await promise for each chunk module + promiseExportName: "__tla", + // The function to generate import names of top-level await promise in each chunk module + promiseImportName: i => `__tla_${i}` + }), + ], // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` // diff --git a/package-lock.json b/package-lock.json index 54a33146..0210a23a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -74,13 +74,15 @@ "i18next": "^23.6.0", "i18next-browser-languagedetector": "^7.1.0", "path-browserify": "^1.0.1", + "pdfjs-dist": "^4.0.189", "react": "^18.2.0", "react-bootstrap": "^2.9.1", "react-dom": "^18.2.0", "react-i18next": "^13.3.1", "react-icons": "^4.11.0", "react-router-bootstrap": "^0.26.2", - "react-router-dom": "^6.18.0" + "react-router-dom": "^6.18.0", + "vite-plugin-top-level-await": "^1.3.1" }, "devDependencies": { "@tauri-apps/cli": "^1.5.0", @@ -2579,7 +2581,6 @@ "cpu": [ "arm" ], - "dev": true, "optional": true, "os": [ "android" @@ -2595,7 +2596,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "android" @@ -2611,7 +2611,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "android" @@ -2627,7 +2626,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "darwin" @@ -2643,7 +2641,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "darwin" @@ -2659,7 +2656,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "freebsd" @@ -2675,7 +2671,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "freebsd" @@ -2691,7 +2686,6 @@ "cpu": [ "arm" ], - "dev": true, "optional": true, "os": [ "linux" @@ -2707,7 +2701,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -2723,7 +2716,6 @@ "cpu": [ "ia32" ], - "dev": true, "optional": true, "os": [ "linux" @@ -2739,7 +2731,6 @@ "cpu": [ "loong64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -2755,7 +2746,6 @@ "cpu": [ "mips64el" ], - "dev": true, "optional": true, "os": [ "linux" @@ -2771,7 +2761,6 @@ "cpu": [ "ppc64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -2787,7 +2776,6 @@ "cpu": [ "riscv64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -2803,7 +2791,6 @@ "cpu": [ "s390x" ], - "dev": true, "optional": true, "os": [ "linux" @@ -2819,7 +2806,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -2835,7 +2821,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "netbsd" @@ -2851,7 +2836,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "openbsd" @@ -2867,7 +2851,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "sunos" @@ -2883,7 +2866,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "win32" @@ -2899,7 +2881,6 @@ "cpu": [ "ia32" ], - "dev": true, "optional": true, "os": [ "win32" @@ -2915,7 +2896,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "win32" @@ -3605,7 +3585,7 @@ "version": "0.3.3", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", - "dev": true, + "devOptional": true, "dependencies": { "@jridgewell/set-array": "^1.0.1", "@jridgewell/sourcemap-codec": "^1.4.10", @@ -3619,7 +3599,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", - "dev": true, + "devOptional": true, "engines": { "node": ">=6.0.0" } @@ -3628,7 +3608,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", - "dev": true, + "devOptional": true, "engines": { "node": ">=6.0.0" } @@ -3637,7 +3617,7 @@ "version": "0.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", - "dev": true, + "devOptional": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.0", "@jridgewell/trace-mapping": "^0.3.9" @@ -3647,13 +3627,13 @@ "version": "1.4.15", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true + "devOptional": true }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.20", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", - "dev": true, + "devOptional": true, "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -3833,6 +3813,22 @@ "react": ">=16.14.0" } }, + "node_modules/@rollup/plugin-virtual": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@rollup/plugin-virtual/-/plugin-virtual-3.0.2.tgz", + "integrity": "sha512-10monEYsBp3scM4/ND4LNH5Rxvh3e/cVeL3jWTgZ2SrQ+BmUoQcopVQvnaMcOnykb1VkxUFuDAN+0FnpTFRy2A==", + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, "node_modules/@sideway/address": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz", @@ -3906,6 +3902,198 @@ "sourcemap-codec": "^1.4.8" } }, + "node_modules/@swc/core": { + "version": "1.3.96", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.3.96.tgz", + "integrity": "sha512-zwE3TLgoZwJfQygdv2SdCK9mRLYluwDOM53I+dT6Z5ZvrgVENmY3txvWDvduzkV+/8IuvrRbVezMpxcojadRdQ==", + "hasInstallScript": true, + "dependencies": { + "@swc/counter": "^0.1.1", + "@swc/types": "^0.1.5" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.3.96", + "@swc/core-darwin-x64": "1.3.96", + "@swc/core-linux-arm-gnueabihf": "1.3.96", + "@swc/core-linux-arm64-gnu": "1.3.96", + "@swc/core-linux-arm64-musl": "1.3.96", + "@swc/core-linux-x64-gnu": "1.3.96", + "@swc/core-linux-x64-musl": "1.3.96", + "@swc/core-win32-arm64-msvc": "1.3.96", + "@swc/core-win32-ia32-msvc": "1.3.96", + "@swc/core-win32-x64-msvc": "1.3.96" + }, + "peerDependencies": { + "@swc/helpers": "^0.5.0" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.3.96", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.3.96.tgz", + "integrity": "sha512-8hzgXYVd85hfPh6mJ9yrG26rhgzCmcLO0h1TIl8U31hwmTbfZLzRitFQ/kqMJNbIBCwmNH1RU2QcJnL3d7f69A==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.3.96", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.3.96.tgz", + "integrity": "sha512-mFp9GFfuPg+43vlAdQZl0WZpZSE8sEzqL7sr/7Reul5McUHP0BaLsEzwjvD035ESfkY8GBZdLpMinblIbFNljQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.3.96", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.3.96.tgz", + "integrity": "sha512-8UEKkYJP4c8YzYIY/LlbSo8z5Obj4hqcv/fUTHiEePiGsOddgGf7AWjh56u7IoN/0uEmEro59nc1ChFXqXSGyg==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.3.96", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.3.96.tgz", + "integrity": "sha512-c/IiJ0s1y3Ymm2BTpyC/xr6gOvoqAVETrivVXHq68xgNms95luSpbYQ28rqaZC8bQC8M5zdXpSc0T8DJu8RJGw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.3.96", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.3.96.tgz", + "integrity": "sha512-i5/UTUwmJLri7zhtF6SAo/4QDQJDH2fhYJaBIUhrICmIkRO/ltURmpejqxsM/ye9Jqv5zG7VszMC0v/GYn/7BQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.3.96", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.3.96.tgz", + "integrity": "sha512-USdaZu8lTIkm4Yf9cogct/j5eqtdZqTgcTib4I+NloUW0E/hySou3eSyp3V2UAA1qyuC72ld1otXuyKBna0YKQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.3.96", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.3.96.tgz", + "integrity": "sha512-QYErutd+G2SNaCinUVobfL7jWWjGTI0QEoQ6hqTp7PxCJS/dmKmj3C5ZkvxRYcq7XcZt7ovrYCTwPTHzt6lZBg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.3.96", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.3.96.tgz", + "integrity": "sha512-hjGvvAduA3Un2cZ9iNP4xvTXOO4jL3G9iakhFsgVhpkU73SGmK7+LN8ZVBEu4oq2SUcHO6caWvnZ881cxGuSpg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.3.96", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.3.96.tgz", + "integrity": "sha512-Far2hVFiwr+7VPCM2GxSmbh3ikTpM3pDombE+d69hkedvYHYZxtTF+2LTKl/sXtpbUnsoq7yV/32c9R/xaaWfw==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.3.96", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.3.96.tgz", + "integrity": "sha512-4VbSAniIu0ikLf5mBX81FsljnfqjoVGleEkCQv4+zRlyZtO3FHoDPkeLVoy6WRlj7tyrRcfUJ4mDdPkbfTO14g==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.2.tgz", + "integrity": "sha512-9F4ys4C74eSTEUNndnER3VJ15oru2NumfQxS8geE+f3eB5xvfxpWyqE5XlVnxb/R14uoXi6SLbBwwiDSkv+XEw==" + }, "node_modules/@swc/helpers": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.3.tgz", @@ -3919,6 +4107,11 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, + "node_modules/@swc/types": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.5.tgz", + "integrity": "sha512-myfUej5naTBWnqOCc/MdVOLVjXUXtIA+NpDrDBKJtLLg2shUjBu3cZmB/85RyitKc55+lUUyl7oRfLOvkr2hsw==" + }, "node_modules/@swiftcarrot/color-fns": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/@swiftcarrot/color-fns/-/color-fns-3.2.0.tgz", @@ -4281,7 +4474,7 @@ "version": "18.18.7", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.7.tgz", "integrity": "sha512-bw+lEsxis6eqJYW8Ql6+yTqkE6RuFtsQPSe5JxXbqYRFQEER5aJA9a5UH9igqDWm3X4iLHIKOHlnAXLM4mi7uQ==", - "dev": true, + "devOptional": true, "dependencies": { "undici-types": "~5.26.4" } @@ -4749,7 +4942,7 @@ "version": "8.10.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", - "dev": true, + "devOptional": true, "bin": { "acorn": "bin/acorn" }, @@ -6751,7 +6944,6 @@ "version": "0.18.20", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", - "dev": true, "hasInstallScript": true, "bin": { "esbuild": "bin/esbuild" @@ -10093,7 +10285,6 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", - "dev": true, "funding": [ { "type": "github", @@ -10810,8 +11001,7 @@ "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" }, "node_modules/picomatch": { "version": "2.3.1", @@ -10862,7 +11052,6 @@ "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", - "dev": true, "funding": [ { "type": "opencollective", @@ -11639,7 +11828,6 @@ "version": "3.29.4", "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", - "dev": true, "bin": { "rollup": "dist/bin/rollup" }, @@ -12042,7 +12230,7 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, + "devOptional": true, "engines": { "node": ">=0.10.0" } @@ -12051,7 +12239,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -12060,7 +12247,7 @@ "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, + "devOptional": true, "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -12476,7 +12663,7 @@ "version": "5.22.0", "resolved": "https://registry.npmjs.org/terser/-/terser-5.22.0.tgz", "integrity": "sha512-hHZVLgRA2z4NWcN6aS5rQDc+7Dcy58HOf2zbYwmFcQ+ua3h6eEFf5lIDKTzbWwlazPyOZsFQO8V80/IjVNExEw==", - "dev": true, + "devOptional": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", @@ -12494,7 +12681,7 @@ "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true + "devOptional": true }, "node_modules/text-table": { "version": "0.2.0", @@ -13038,7 +13225,7 @@ "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true + "devOptional": true }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.0", @@ -13242,7 +13429,6 @@ "version": "4.5.0", "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.0.tgz", "integrity": "sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==", - "dev": true, "dependencies": { "esbuild": "^0.18.10", "postcss": "^8.4.27", @@ -13398,6 +13584,31 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/vite-plugin-top-level-await": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/vite-plugin-top-level-await/-/vite-plugin-top-level-await-1.3.1.tgz", + "integrity": "sha512-55M1h4NAwkrpxPNOJIBzKZFihqLUzIgnElLSmPNPMR2Fn9+JHKaNg3sVX1Fq+VgvuBksQYxiD3OnwQAUu7kaPQ==", + "dependencies": { + "@rollup/plugin-virtual": "^3.0.1", + "@swc/core": "^1.3.10", + "uuid": "^9.0.0" + }, + "peerDependencies": { + "vite": ">=2.8" + } + }, + "node_modules/vite-plugin-top-level-await/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/vitest": { "version": "0.32.4", "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.32.4.tgz", diff --git a/server-node/src/routes/api/api-controller.ts b/server-node/src/routes/api/api-controller.ts index 9524802e..d119b530 100644 --- a/server-node/src/routes/api/api-controller.ts +++ b/server-node/src/routes/api/api-controller.ts @@ -2,6 +2,7 @@ import express, { Request, Response } from 'express'; //import workflow from './workflow-controller'; import operations from './operations-controller'; +import conversions from './conversions-controller'; const router = express.Router(); @@ -11,6 +12,7 @@ router.get("/", (req: Request, res: Response) => { }); router.use("/operations", operations); +router.use("/conversions", conversions); //router.use("/workflow", workflow); export default router; \ No newline at end of file diff --git a/server-node/src/routes/api/conversions-controller.ts b/server-node/src/routes/api/conversions-controller.ts new file mode 100644 index 00000000..8deb6144 --- /dev/null +++ b/server-node/src/routes/api/conversions-controller.ts @@ -0,0 +1,27 @@ + +import { respondWithPdfFile, response_mustHaveExactlyOneFile, response_dependencyNotConfigured } from '../../utils/endpoint-utils'; +import { fileToPdf, isLibreOfficeInstalled } from '../../utils/libre-office-utils'; + +import express, { Request, Response } from 'express'; +const router = express.Router(); +import multer from 'multer'; +const upload = multer(); +import Joi from 'joi'; + +router.post('/file-to-pdf', upload.single("file"), async function(req: Request, res: Response) { + if (!req.file) { + response_mustHaveExactlyOneFile(res); + return; + } + + const isInstalled = await isLibreOfficeInstalled(); + if (isInstalled) { + const outputFile = await fileToPdf(req.file.buffer, req.file.originalname); + respondWithPdfFile(res, outputFile); + return; + } + + response_dependencyNotConfigured(res, "LibreOffice"); +}); + +export default router; diff --git a/server-node/src/utils/endpoint-utils.ts b/server-node/src/utils/endpoint-utils.ts index 2933da1f..53ac17ab 100644 --- a/server-node/src/utils/endpoint-utils.ts +++ b/server-node/src/utils/endpoint-utils.ts @@ -2,14 +2,18 @@ import { Response } from 'express'; import { PdfFile } from '@stirling-pdf/shared-operations/wrappers/PdfFile' +export async function respondWithFile(res: Response, bytes: Uint8Array, name: string, mimeType: string): Promise { + res.writeHead(200, { + 'Content-Type': mimeType, + 'Content-disposition': 'attachment;filename=' + name, + 'Content-Length': bytes.length + }); + res.end(bytes); +} + export async function respondWithPdfFile(res: Response, file: PdfFile): Promise { const byteFile = await file.convertToByteArrayFile(); - res.writeHead(200, { - 'Content-Type': "application/pdf", - 'Content-disposition': 'attachment;filename=' + byteFile.filename, - 'Content-Length': byteFile.byteArray?.length - }); - res.end(byteFile.byteArray) + respondWithFile(res, byteFile.byteArray!, byteFile.filename, "application/pdf"); } export function response_mustHaveExactlyOneFile(res: Response): void { @@ -27,3 +31,12 @@ export function response_mustHaveExactlyOneFile(res: Response): void { } ]); } + +export function response_dependencyNotConfigured(res: Response, dependencyName: string): void { + res.status(400).send([ + { + "message": `${dependencyName} is not configured correctly on the server.`, + "type": "dependency_error", + } + ]); +} diff --git a/server-node/src/utils/libre-office-utils.ts b/server-node/src/utils/libre-office-utils.ts new file mode 100644 index 00000000..548876f6 --- /dev/null +++ b/server-node/src/utils/libre-office-utils.ts @@ -0,0 +1,99 @@ + +import fs from 'fs'; +import os from 'os'; +import path from 'path'; +import { exec, spawn } from 'child_process' +import { PdfFile, fromUint8Array } from '@stirling-pdf/shared-operations/wrappers/PdfFile' + +export async function fileToPdf(byteArray: Uint8Array, filename: string): Promise { + const parentDir = path.join(os.tmpdir(), "StirlingPDF"); + fs.mkdirSync(parentDir, {recursive: true}); + const tempDir = fs.mkdtempSync(parentDir+"/"); + const srcFile = path.join(tempDir, filename); + const randFolderName = path.parse(tempDir).base; + + await writeBytesToFile(srcFile, byteArray); + + const messages = await runLibreOfficeCommand(randFolderName, ["--headless","--convert-to","pdf",srcFile,"--outdir",tempDir]); + const lastMessage = messages[messages.length-1] + const outputFilePath = lastMessage.split(" -> ")[1].split(".pdf")[0]+".pdf"; + const outputFileName = path.parse(outputFilePath).base; + const outputBytes = await readBytesFromFile(outputFilePath); + + fs.rmdirSync(tempDir); + + return fromUint8Array(outputBytes, outputFileName); +} + +export function isLibreOfficeInstalled() { + return new Promise((resolve, reject) => { + exec("libreoffice --version", (error, stdout, stderr) => { + if (error) { + resolve(false); + return; + } + if (stderr) { + resolve(false); + return; + } + const result = stdout.match("LibreOffice ([0-9]+\.){4}.*"); + resolve(result ? true : false); + }); + }) +} + +function writeBytesToFile(filePath: string, bytes: Uint8Array): Promise { + return new Promise((resolve, reject) => { + fs.writeFile(filePath, bytes, function(err) { + if(err) { + reject(err) + return; + } + resolve(); + }); + }); +} + +function readBytesFromFile(filePath: string): Promise { + return new Promise((resolve, reject) => { + fs.readFile(filePath, (err, data) => { + if (err) { + reject(new Error(`Error reading file: ${err.message}`)); + } else { + const uint8Array = new Uint8Array(data); + resolve(uint8Array); + } + }); + }); +} + +function runLibreOfficeCommand(idKey: string, args: string[]): Promise { + return new Promise(async (resolve, reject) => { + const messageList: string[] = []; + + const process = spawn("libreoffice", args); + + process.stdout.on('data', (data) => { + const dataStr = data.toString(); + console.log(`Progress ${idKey}:`, dataStr); + messageList.push(dataStr); + }); + + process.stderr.on('data', (data) => { + console.error(`stderr ${idKey}:`, data.toString()); + }); + + process.on('exit', (code) => { + if (code === 0) { + resolve(messageList); + } else { + reject(new Error(`Command failed with exit code ${code}`)); + } + }); + + process.on('error', (err) => { + reject(err); + }); + + }); +} diff --git a/shared-operations/functions/splitPDF.ts b/shared-operations/functions/splitPDF.ts index edf7597d..20137e80 100644 --- a/shared-operations/functions/splitPDF.ts +++ b/shared-operations/functions/splitPDF.ts @@ -1,6 +1,4 @@ -import { PDFDocument } from 'pdf-lib'; - import { selectPages } from "./subDocumentFunctions"; import { PdfFile } from '../wrappers/PdfFile'; diff --git a/shared-operations/functions/updateMetadata.ts b/shared-operations/functions/updateMetadata.ts index 5e16019b..97165bcd 100644 --- a/shared-operations/functions/updateMetadata.ts +++ b/shared-operations/functions/updateMetadata.ts @@ -1,5 +1,4 @@ -import { PDFDocument, ParseSpeeds } from 'pdf-lib'; import { PdfFile, fromPdfLib } from '../wrappers/PdfFile'; export type Metadata = {