From 6fab84561f45ecd8c240e94e010481ebc0da12aa Mon Sep 17 00:00:00 2001 From: max06bayer <101212752+max06bayer@users.noreply.github.com> Date: Sat, 18 Jul 2026 14:27:23 +0200 Subject: [PATCH 01/32] Add native Windows and Linux desktop builds --- .github/workflows/desktop-native-builds.yml | 119 ++++++++++ .gitignore | 1 + desktop/README.md | 28 ++- desktop/package.json | 4 +- desktop/runtime/chrome.js | 114 ++++++++- desktop/runtime/native-tool-linux-wrapper.sh | 7 + desktop/runtime/pdf2htmlEX-linux-wrapper.sh | 19 ++ desktop/runtime/supervise.ps1 | 26 +++ desktop/scripts/build.mjs | 17 ++ desktop/scripts/prepare-linux-runtimes.sh | 80 +++++++ desktop/scripts/prepare-windows-runtimes.ps1 | 80 +++++++ desktop/scripts/prepare.mjs | 234 +++++++++++-------- desktop/scripts/smoke-runtimes.mjs | 95 ++++++++ desktop/src-tauri/Cargo.lock | 126 +++++++++- desktop/src-tauri/Cargo.toml | 3 +- desktop/src-tauri/src/lib.rs | 218 ++++++++++++----- desktop/src-tauri/tauri.conf.json | 12 +- 17 files changed, 1011 insertions(+), 172 deletions(-) create mode 100644 .github/workflows/desktop-native-builds.yml create mode 100644 desktop/runtime/native-tool-linux-wrapper.sh create mode 100644 desktop/runtime/pdf2htmlEX-linux-wrapper.sh create mode 100644 desktop/runtime/supervise.ps1 create mode 100644 desktop/scripts/build.mjs create mode 100644 desktop/scripts/prepare-linux-runtimes.sh create mode 100644 desktop/scripts/prepare-windows-runtimes.ps1 create mode 100644 desktop/scripts/smoke-runtimes.mjs diff --git a/.github/workflows/desktop-native-builds.yml b/.github/workflows/desktop-native-builds.yml new file mode 100644 index 0000000..280493e --- /dev/null +++ b/.github/workflows/desktop-native-builds.yml @@ -0,0 +1,119 @@ +name: Desktop native builds + +on: + workflow_dispatch: + pull_request: + paths: + - '.github/workflows/desktop-native-builds.yml' + - 'backend/**' + - 'desktop/**' + - 'package-lock.json' + - 'package.json' + - 'public/**' + - 'scripts/**' + - 'src/**' + +permissions: + contents: read + +jobs: + windows-x64: + name: Windows x64 offline installer + runs-on: windows-2025 + timeout-minutes: 90 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 24.18.0 + cache: npm + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '17' + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + with: + workspaces: desktop/src-tauri -> target + - name: Install JavaScript dependencies + shell: pwsh + run: | + npm ci + npm ci --prefix desktop + - name: Install bundled Python conversion packages + shell: pwsh + run: python -m pip install --disable-pip-version-check pillow python-docx lxml openpyxl python-pptx xlsxwriter + - name: Prepare native Windows runtimes + shell: pwsh + run: ./desktop/scripts/prepare-windows-runtimes.ps1 -RuntimeRoot "$pwd/desktop/.native-runtime/windows" + - name: Build and smoke-test Windows package + shell: pwsh + working-directory: desktop + env: + DOCUFLEX_PDF2HTMLEX_RUNTIME: ${{ github.workspace }}/desktop/.native-runtime/windows/pdf2htmlEX + DOCUFLEX_OCR_RUNTIME: ${{ github.workspace }}/desktop/.native-runtime/windows/ocr + DOCUFLEX_OFFICE_RUNTIME: ${{ github.workspace }}/desktop/.native-runtime/windows/office + run: | + $env:DOCUFLEX_PYTHON_RUNTIME = $env:pythonLocation + npm run build + - name: Upload Windows installer + uses: actions/upload-artifact@v4 + with: + name: Docuflex-Windows-x64 + if-no-files-found: error + path: desktop/src-tauri/target/release/bundle/nsis/*.exe + + linux-x64: + name: Linux x64 AppImage and Debian package + runs-on: ubuntu-22.04 + timeout-minutes: 90 + steps: + - uses: actions/checkout@v4 + - name: Install Tauri Linux build dependencies + run: | + sudo apt-get update + sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libfuse2 + - uses: actions/setup-node@v4 + with: + node-version: 24.18.0 + cache: npm + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '17' + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + with: + workspaces: desktop/src-tauri -> target + - name: Install JavaScript dependencies + run: | + npm ci + npm ci --prefix desktop + - name: Install bundled Python conversion packages + run: python -m pip install --disable-pip-version-check pillow python-docx lxml openpyxl python-pptx xlsxwriter + - name: Prepare native Linux runtimes + run: | + chmod +x desktop/scripts/prepare-linux-runtimes.sh desktop/runtime/*.sh + desktop/scripts/prepare-linux-runtimes.sh "$GITHUB_WORKSPACE/desktop/.native-runtime/linux" + - name: Build and smoke-test Linux packages + working-directory: desktop + env: + DOCUFLEX_PDF2HTMLEX_RUNTIME: ${{ github.workspace }}/desktop/.native-runtime/linux/pdf2htmlEX + DOCUFLEX_OCR_RUNTIME: ${{ github.workspace }}/desktop/.native-runtime/linux/ocr + DOCUFLEX_OFFICE_RUNTIME: ${{ github.workspace }}/desktop/.native-runtime/linux/office + DOCUFLEX_PYTHON_RUNTIME: ${{ env.pythonLocation }} + run: npm run build + - name: Upload Linux packages + uses: actions/upload-artifact@v4 + with: + name: Docuflex-Linux-x64 + if-no-files-found: error + path: | + desktop/src-tauri/target/release/bundle/appimage/*.AppImage + desktop/src-tauri/target/release/bundle/deb/*.deb diff --git a/.gitignore b/.gitignore index e559c55..8998dc3 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ tmp/ .pdf2htmlex/ .ocr/ .document-python/ +desktop/.native-runtime/ # Environment files .env diff --git a/desktop/README.md b/desktop/README.md index 3252e44..3ce1c40 100644 --- a/desktop/README.md +++ b/desktop/README.md @@ -1,12 +1,18 @@ -# Docuflex for macOS +# Docuflex desktop builds -This directory packages the existing Docuflex `/editor` route as a native Tauri app. The web application source remains unchanged. +The desktop package runs the existing `/editor` application and its Java backend entirely on loopback. Each native package contains Node.js, a reduced Java 17 runtime, Python conversion libraries, OCR tools and language data, `pdf2htmlEX`, and an office converter. Runtime staging fails instead of silently using tools installed on the end user's computer. -The packaged app applies its macOS-only titlebar and sidebar arrangement from `runtime/chrome.js`; those overrides are injected by Tauri and are not included in the website. +## Native packages -## Build +- macOS Apple Silicon: `npm run build` in `desktop/` creates `Docuflex.app`. +- Windows x64: the `Desktop native builds` workflow creates a current-user NSIS installer and embeds the offline WebView2 installer. +- Linux x64: the same workflow creates an AppImage and a Debian package on Ubuntu 22.04. -The current native bundle targets Apple Silicon macOS 26 or newer. +Windows and Linux are deliberately compiled on native GitHub runners. Tauri recommends native CI for installers, and this also ensures that every bundled helper executable matches the target operating system. + +Before packaging, `npm run smoke` verifies the bundled Node and Java runtimes, Python imports, all three OCR language files, real OCR output, a real PDF-to-HTML conversion that preserves text, and LibreOffice availability. A failed native dependency prevents the installer artifact from being uploaded. + +For a local Apple Silicon build: ```sh cd desktop @@ -14,12 +20,12 @@ npm install npm run build ``` -The staging step builds the existing SvelteKit application and PDFBox server, downloads and verifies the pinned Node.js LTS runtime, creates a trimmed Java 17 runtime with `jlink`, bundles the native Apple Silicon `pdf2htmlEX` and OCR runtimes, and generates the macOS icon from `public/macos-icon-iOS-Default-1024x1024@1x.png`. - -The resulting app is written below `desktop/src-tauri/target/release/bundle/macos/Docuflex.app`. +The app is written to `desktop/src-tauri/target/release/bundle/macos/Docuflex.app`. It targets macOS 26 or newer and uses the icon exported at `public/macos-icon-iOS-Default-1024x1024@1x.png`. -The app runs its frontend and PDFBox services only on `127.0.0.1`. No hosted Docuflex backend is used. Edit Text uses the bundled native `pdf2htmlEX`, Poppler, FontForge, data files, and relocated dylibraries, so conversion works offline without Homebrew, MacPorts, Docker, or a Linux compatibility layer. +The relocatable macOS converter and OCR inputs remain under `vendor/pdf2htmlEX-macos-arm64` and `vendor/ocr-macos-arm64`. Their pinned source versions and checksums are recorded in `runtime/pdf2htmlEX-SOURCE.txt` and the vendor `SOURCE.txt` files; the existing bundling scripts can recreate them from native builds. -The converter bundle is under `vendor/pdf2htmlEX-macos-arm64`. Its pinned source versions and checksums are recorded in `runtime/pdf2htmlEX-SOURCE.txt`; `scripts/bundle-pdf2htmlex.mjs` recreates the relocatable runtime after the native sources have been built. +## Window chrome -Offline OCR uses bundled Apple Silicon builds of Tesseract, `pdftoppm`, and `pdfunite`, plus pinned English, German, and orientation language data. `scripts/bundle-ocr.mjs` recreates that relocatable runtime under `vendor/ocr-macos-arm64`. +- macOS retains the integrated traffic lights and desktop-specific sidebar/header alignment. +- Windows uses the web header with a draggable native window frame and Windows controls on the right; the utility area reserves the corresponding space. +- Linux uses the normal desktop window decoration and the web header unchanged. diff --git a/desktop/package.json b/desktop/package.json index 3e5d5af..27a9623 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -5,7 +5,9 @@ "type": "module", "scripts": { "stage": "node scripts/prepare.mjs", - "build": "npm run stage && tauri build --bundles app", + "smoke": "node scripts/smoke-runtimes.mjs", + "bundle": "node scripts/build.mjs", + "build": "npm run stage && npm run smoke && npm run bundle", "dev": "npm run stage && tauri dev" }, "devDependencies": { diff --git a/desktop/runtime/chrome.js b/desktop/runtime/chrome.js index 890f4ec..b029ae0 100644 --- a/desktop/runtime/chrome.js +++ b/desktop/runtime/chrome.js @@ -1,4 +1,9 @@ (() => { + const desktopPlatform = /Windows/i.test(navigator.userAgent) + ? 'windows' + : /Linux/i.test(navigator.userAgent) + ? 'linux' + : 'macos'; const desktopBlobUrls = new Map(); const originalCreateObjectUrl = URL.createObjectURL.bind(URL); const originalRevokeObjectUrl = URL.revokeObjectURL.bind(URL); @@ -249,6 +254,91 @@ position: absolute; } + html[data-docuflex-desktop="windows"] .topbar, + html[data-docuflex-desktop="windows"] .brand-area, + html[data-docuflex-desktop="windows"] .tab-strip, + html[data-docuflex-desktop="windows"] .utilities { + -webkit-app-region: drag; + } + + html[data-docuflex-desktop="windows"] .utilities { + padding-right: 138px !important; + } + + html[data-docuflex-desktop="windows"] .topbar button, + html[data-docuflex-desktop="windows"] .topbar a, + html[data-docuflex-desktop="windows"] .topbar input, + html[data-docuflex-desktop="windows"] .docuflex-windows-controls { + -webkit-app-region: no-drag; + } + + .docuflex-windows-controls { + display: none; + } + + html[data-docuflex-desktop="windows"] .docuflex-windows-controls { + display: flex; + height: 56px; + position: fixed; + right: 0; + top: 0; + z-index: 10002; + } + + .docuflex-windows-control { + align-items: center; + background: transparent; + border: 0; + color: #616161; + display: flex; + height: 56px; + justify-content: center; + padding: 0; + position: relative; + width: 46px; + } + + .docuflex-windows-control:hover { + background: rgba(0, 0, 0, 0.07); + color: #171717; + } + + .docuflex-windows-control.close:hover { + background: #c42b1c; + color: #fff; + } + + .docuflex-windows-control::before, + .docuflex-windows-control::after { + box-sizing: border-box; + content: ""; + position: absolute; + } + + .docuflex-windows-control.minimize::before { + border-top: 1px solid currentColor; + height: 1px; + width: 10px; + } + + .docuflex-windows-control.maximize::before { + border: 1px solid currentColor; + height: 10px; + width: 10px; + } + + .docuflex-windows-control.close::before, + .docuflex-windows-control.close::after { + background: currentColor; + height: 1px; + transform: rotate(45deg); + width: 12px; + } + + .docuflex-windows-control.close::after { + transform: rotate(-45deg); + } + .docuflex-desktop-export-menu { -webkit-backdrop-filter: blur(18px); animation: docuflex-desktop-export-menu-in 125ms cubic-bezier(0.215, 0.61, 0.355, 1); @@ -294,7 +384,7 @@ `; const installDesktopChrome = () => { - document.documentElement.dataset.docuflexDesktop = 'macos'; + document.documentElement.dataset.docuflexDesktop = desktopPlatform; if (!document.getElementById('docuflex-desktop-chrome')) { const style = document.createElement('style'); @@ -304,6 +394,7 @@ } const markDragRegion = () => { + if (desktopPlatform === 'linux') return; document .querySelectorAll('.topbar, .brand-area, .tab-strip, .utilities') .forEach((element) => element.setAttribute('data-tauri-drag-region', 'deep')); @@ -314,6 +405,27 @@ childList: true, subtree: true, }); + + if (desktopPlatform === 'windows' && !document.querySelector('.docuflex-windows-controls')) { + const controls = document.createElement('div'); + controls.className = 'docuflex-windows-controls'; + controls.setAttribute('aria-label', 'Window controls'); + for (const [action, label] of [ + ['minimize', 'Minimize'], + ['maximize', 'Maximize or restore'], + ['close', 'Close'], + ]) { + const button = document.createElement('button'); + button.className = `docuflex-windows-control ${action}`; + button.type = 'button'; + button.setAttribute('aria-label', label); + button.addEventListener('click', () => { + window.location.assign(`/__docuflex/window/${action}`); + }); + controls.append(button); + } + document.body.append(controls); + } }; if (document.readyState === 'loading') { diff --git a/desktop/runtime/native-tool-linux-wrapper.sh b/desktop/runtime/native-tool-linux-wrapper.sh new file mode 100644 index 0000000..0dbbd88 --- /dev/null +++ b/desktop/runtime/native-tool-linux-wrapper.sh @@ -0,0 +1,7 @@ +#!/bin/sh +set -eu + +SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +TOOL_NAME=${0##*/} +export LD_LIBRARY_PATH="$SCRIPT_DIR/../lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" +exec "$SCRIPT_DIR/$TOOL_NAME-native" "$@" diff --git a/desktop/runtime/pdf2htmlEX-linux-wrapper.sh b/desktop/runtime/pdf2htmlEX-linux-wrapper.sh new file mode 100644 index 0000000..2d117b8 --- /dev/null +++ b/desktop/runtime/pdf2htmlEX-linux-wrapper.sh @@ -0,0 +1,19 @@ +#!/bin/bash +set -eu + +SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +APP_ROOT="$SCRIPT_DIR/../app" + +normalized_args=() +while [ "$#" -gt 0 ]; do + if [ "$1" = "--embed" ] && [ "$#" -ge 2 ] && [ "$2" = "1" ]; then + normalized_args+=(--embed-css 1 --embed-font 1 --embed-image 1 --embed-javascript 1 --embed-outline 1) + shift 2 + continue + fi + normalized_args+=("$1") + shift +done + +export APPDIR="$APP_ROOT" +exec "$APP_ROOT/AppRun" "${normalized_args[@]}" diff --git a/desktop/runtime/supervise.ps1 b/desktop/runtime/supervise.ps1 new file mode 100644 index 0000000..0e591fb --- /dev/null +++ b/desktop/runtime/supervise.ps1 @@ -0,0 +1,26 @@ +param( + [Parameter(Mandatory = $true)] + [int]$DocuflexParentPid, + [Parameter(Mandatory = $true)] + [string]$FilePath, + [Parameter(ValueFromRemainingArguments = $true)] + [string[]]$ProgramArguments +) + +$ErrorActionPreference = 'Stop' +if ($ProgramArguments.Count -gt 0 -and $ProgramArguments[0] -eq '--') { + $ProgramArguments = $ProgramArguments[1..($ProgramArguments.Count - 1)] +} + +$service = Start-Process -FilePath $FilePath -ArgumentList $ProgramArguments -NoNewWindow -PassThru +try { + while (-not $service.HasExited -and (Get-Process -Id $DocuflexParentPid -ErrorAction SilentlyContinue)) { + Start-Sleep -Milliseconds 500 + } +} +finally { + if (-not $service.HasExited) { + & taskkill.exe /PID $service.Id /T /F 2>$null | Out-Null + } + $service.WaitForExit() +} diff --git a/desktop/scripts/build.mjs b/desktop/scripts/build.mjs new file mode 100644 index 0000000..363cede --- /dev/null +++ b/desktop/scripts/build.mjs @@ -0,0 +1,17 @@ +import { execFileSync } from 'node:child_process'; +import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const desktopRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const tauriCli = join(desktopRoot, 'node_modules', '@tauri-apps', 'cli', 'tauri.js'); +const bundles = process.platform === 'darwin' + ? ['app'] + : process.platform === 'win32' + ? ['nsis'] + : ['appimage', 'deb']; + +execFileSync(process.execPath, [tauriCli, 'build', '--bundles', bundles.join(',')], { + cwd: desktopRoot, + env: process.env, + stdio: 'inherit' +}); diff --git a/desktop/scripts/prepare-linux-runtimes.sh b/desktop/scripts/prepare-linux-runtimes.sh new file mode 100644 index 0000000..8e0d4ae --- /dev/null +++ b/desktop/scripts/prepare-linux-runtimes.sh @@ -0,0 +1,80 @@ +#!/bin/bash +set -euo pipefail + +RUNTIME_ROOT=${1:?Pass the Linux runtime output directory.} +case "$RUNTIME_ROOT" in + */desktop/.native-runtime/linux) ;; + *) echo "Refusing unexpected runtime output path: $RUNTIME_ROOT" >&2; exit 2 ;; +esac + +sudo apt-get update +sudo apt-get install -y --no-install-recommends \ + curl libarchive-tools libreoffice-writer pax-utils poppler-utils \ + tesseract-ocr tesseract-ocr-deu tesseract-ocr-eng tesseract-ocr-osd + +rm -rf -- "$RUNTIME_ROOT" +mkdir -p "$RUNTIME_ROOT/pdf2htmlEX/bin" "$RUNTIME_ROOT/pdf2htmlEX/share" \ + "$RUNTIME_ROOT/ocr/bin" "$RUNTIME_ROOT/ocr/lib" "$RUNTIME_ROOT/ocr/share/tessdata" \ + "$RUNTIME_ROOT/office" + +PDF2HTMLEX_URL=https://github.com/pdf2htmlEX/pdf2htmlEX/releases/download/v0.18.8.rc1/pdf2htmlEX-0.18.8.rc1-master-20200630-Ubuntu-focal-x86_64.AppImage +PDF2HTMLEX_SHA256=11de2583a3abce5f141fd7fafb1fea2c67b15886e546d6b7675c600012e6ab8c +PDF2HTMLEX_IMAGE="$RUNTIME_ROOT/pdf2htmlEX.AppImage" +curl -L --fail --retry 3 "$PDF2HTMLEX_URL" -o "$PDF2HTMLEX_IMAGE" +echo "$PDF2HTMLEX_SHA256 $PDF2HTMLEX_IMAGE" | sha256sum --check --strict +chmod +x "$PDF2HTMLEX_IMAGE" +EXTRACT_ROOT="$RUNTIME_ROOT/pdf2htmlEX-extract" +mkdir -p "$EXTRACT_ROOT" +(cd "$EXTRACT_ROOT" && "$PDF2HTMLEX_IMAGE" --appimage-extract >/dev/null) +mv "$EXTRACT_ROOT/squashfs-root" "$RUNTIME_ROOT/pdf2htmlEX/app" +cp -a "$RUNTIME_ROOT/pdf2htmlEX/app/usr/local/share/pdf2htmlEX" "$RUNTIME_ROOT/pdf2htmlEX/share/pdf2htmlEX" +cp desktop/runtime/pdf2htmlEX-linux-wrapper.sh "$RUNTIME_ROOT/pdf2htmlEX/bin/pdf2htmlEX" +chmod +x "$RUNTIME_ROOT/pdf2htmlEX/bin/pdf2htmlEX" +cat > "$RUNTIME_ROOT/pdf2htmlEX/SOURCE.txt" <<'EOF' +Linux x86_64 pdf2htmlEX 0.18.8.rc1 official Ubuntu focal AppImage. +Source: https://github.com/pdf2htmlEX/pdf2htmlEX/tree/v0.18.8.rc1 +Archive SHA-256: 11de2583a3abce5f141fd7fafb1fea2c67b15886e546d6b7675c600012e6ab8c +EOF +rm -rf -- "$EXTRACT_ROOT" "$PDF2HTMLEX_IMAGE" + +copy_elf_dependencies() { + local executable=$1 + while IFS= read -r library; do + case "$library" in + /lib/*|/lib64/*|/usr/lib/*) + cp -L -n "$library" "$RUNTIME_ROOT/ocr/lib/$(basename "$library")" || true + ;; + esac + done < <(lddtree -l "$executable") +} + +for tool in pdftoppm pdfunite tesseract; do + source_path=$(command -v "$tool") + cp -L "$source_path" "$RUNTIME_ROOT/ocr/bin/$tool-native" + cp desktop/runtime/native-tool-linux-wrapper.sh "$RUNTIME_ROOT/ocr/bin/$tool" + chmod +x "$RUNTIME_ROOT/ocr/bin/$tool" "$RUNTIME_ROOT/ocr/bin/$tool-native" + copy_elf_dependencies "$source_path" +done + +for data in eng deu osd; do + source_data=$(find /usr/share -path "*/tessdata/$data.traineddata" -print -quit) + test -n "$source_data" + cp "$source_data" "$RUNTIME_ROOT/ocr/share/tessdata/$data.traineddata" +done +mkdir -p "$RUNTIME_ROOT/ocr/licenses" +for package in poppler-utils tesseract-ocr tesseract-ocr-eng tesseract-ocr-deu tesseract-ocr-osd; do + if [ -f "/usr/share/doc/$package/copyright" ]; then + cp "/usr/share/doc/$package/copyright" "$RUNTIME_ROOT/ocr/licenses/$package-copyright.txt" + fi +done + +cp -a /usr/lib/libreoffice/. "$RUNTIME_ROOT/office/" +chmod +x "$RUNTIME_ROOT/office/program/soffice" "$RUNTIME_ROOT/office/program/soffice.bin" + +cat > "$RUNTIME_ROOT/ocr/SOURCE.txt" <<'EOF' +Linux x86_64 native runtime prepared on Ubuntu 22.04. +OCR: Ubuntu native Tesseract and Poppler packages with English, German, and OSD data. +EOF +cat > "$RUNTIME_ROOT/office/SOURCE.txt" <<'EOF' +Linux x86_64 native LibreOffice Writer runtime prepared from the Ubuntu 22.04 package. +EOF diff --git a/desktop/scripts/prepare-windows-runtimes.ps1 b/desktop/scripts/prepare-windows-runtimes.ps1 new file mode 100644 index 0000000..2d50f05 --- /dev/null +++ b/desktop/scripts/prepare-windows-runtimes.ps1 @@ -0,0 +1,80 @@ +param( + [Parameter(Mandatory = $true)] + [string]$RuntimeRoot +) + +$ErrorActionPreference = 'Stop' +$RuntimeRoot = [IO.Path]::GetFullPath($RuntimeRoot) +$expectedSuffix = [IO.Path]::Combine('desktop', '.native-runtime', 'windows') +if (-not $RuntimeRoot.EndsWith($expectedSuffix, [StringComparison]::OrdinalIgnoreCase)) { + throw "Refusing unexpected runtime output path: $RuntimeRoot" +} + +function Get-VerifiedArchive { + param([string]$Url, [string]$Path, [string]$Sha256) + Invoke-WebRequest -Uri $Url -OutFile $Path -UseBasicParsing + $actual = (Get-FileHash -Path $Path -Algorithm SHA256).Hash.ToLowerInvariant() + if ($actual -ne $Sha256) { throw "Checksum failed for $Url" } +} + +if (Test-Path $RuntimeRoot) { Remove-Item -LiteralPath $RuntimeRoot -Recurse -Force } +New-Item -ItemType Directory -Force -Path $RuntimeRoot | Out-Null + +$pdfArchive = Join-Path $RuntimeRoot 'pdf2htmlEX.zip' +Get-VerifiedArchive -Url 'https://soft.rubypdf.com/download/pdf2htmlex/pdf2htmlEX-win32-0.14.6-with-poppler-data.zip' -Path $pdfArchive -Sha256 'e92aa55699c3e9d9b4b4954bea157e59b5c3363cbe9a7713495c553544026354' +$pdfExtract = Join-Path $RuntimeRoot 'pdf2htmlEX-extract' +Expand-Archive -LiteralPath $pdfArchive -DestinationPath $pdfExtract +$pdfRuntime = Join-Path $RuntimeRoot 'pdf2htmlEX' +New-Item -ItemType Directory -Force -Path (Join-Path $pdfRuntime 'bin'), (Join-Path $pdfRuntime 'share') | Out-Null +Copy-Item (Join-Path $pdfExtract 'pdf2htmlEX.exe') (Join-Path $pdfRuntime 'bin/pdf2htmlEX.exe') +Copy-Item (Join-Path $pdfExtract 'data') (Join-Path $pdfRuntime 'share/pdf2htmlEX') -Recurse +Copy-Item (Join-Path $pdfExtract 'LICENSE*') $pdfRuntime +@' +Windows native static pdf2htmlEX 0.14.6 with poppler-data. +Source and binary distribution: https://soft.rubypdf.com/software/pdf2htmlex-windows-version +Archive SHA-256: e92aa55699c3e9d9b4b4954bea157e59b5c3363cbe9a7713495c553544026354 +'@ | Set-Content -Path (Join-Path $pdfRuntime 'SOURCE.txt') -Encoding UTF8 +Remove-Item -LiteralPath $pdfArchive, $pdfExtract -Recurse -Force + +choco install tesseract --yes --no-progress +$tesseractRoot = Join-Path $env:ProgramFiles 'Tesseract-OCR' +if (-not (Test-Path (Join-Path $tesseractRoot 'tesseract.exe'))) { throw 'Tesseract installation was not found.' } + +$popplerArchive = Join-Path $RuntimeRoot 'poppler.zip' +Get-VerifiedArchive -Url 'https://github.com/oschwartz10612/poppler-windows/releases/download/v26.02.0-0/Release-26.02.0-0.zip' -Path $popplerArchive -Sha256 '993e4a94376ed712fafc7058d724ea0b943d118bbd2305cd9ed55174eb85cda5' +$popplerExtract = Join-Path $RuntimeRoot 'poppler-extract' +Expand-Archive -LiteralPath $popplerArchive -DestinationPath $popplerExtract +$popplerBin = Join-Path $popplerExtract 'poppler-26.02.0/Library/bin' +$ocrRuntime = Join-Path $RuntimeRoot 'ocr' +New-Item -ItemType Directory -Force -Path (Join-Path $ocrRuntime 'bin'), (Join-Path $ocrRuntime 'poppler/bin'), (Join-Path $ocrRuntime 'share/tessdata') | Out-Null +Copy-Item (Join-Path $tesseractRoot '*') (Join-Path $ocrRuntime 'bin') -Recurse -Force +Copy-Item (Join-Path $popplerBin '*') (Join-Path $ocrRuntime 'poppler/bin') -Recurse -Force +Copy-Item (Join-Path $popplerExtract 'poppler-26.02.0/Library/share') (Join-Path $ocrRuntime 'share/poppler') -Recurse -Force + +$trainedData = @{ + eng = '7d4322bd2a7749724879683fc3912cb542f19906c83bcc1a52132556427170b2' + deu = '19d219bbb6672c869d20a9636c6816a81eb9a71796cb93ebe0cb1530e2cdb22d' + osd = '9cf5d576fcc47564f11265841e5ca839001e7e6f38ff7f7aacf46d15a96b00ff' +} +foreach ($language in $trainedData.Keys) { + $destination = Join-Path $ocrRuntime "share/tessdata/$language.traineddata" + Invoke-WebRequest -Uri "https://raw.githubusercontent.com/tesseract-ocr/tessdata_fast/4.1.0/$language.traineddata" -OutFile $destination -UseBasicParsing + $actual = (Get-FileHash -Path $destination -Algorithm SHA256).Hash.ToLowerInvariant() + if ($actual -ne $trainedData[$language]) { throw "Checksum failed for $language.traineddata" } +} +Remove-Item -LiteralPath $popplerArchive, $popplerExtract -Recurse -Force +@' +Windows native OCR runtime. +Tesseract 5 package: https://community.chocolatey.org/packages/tesseract +Poppler 26.02.0: https://github.com/oschwartz10612/poppler-windows/releases/tag/v26.02.0-0 +Tesseract fast language data 4.1.0: https://github.com/tesseract-ocr/tessdata_fast/tree/4.1.0 +'@ | Set-Content -Path (Join-Path $ocrRuntime 'SOURCE.txt') -Encoding UTF8 + +choco install libreoffice-fresh --yes --no-progress +$officeSource = Join-Path $env:ProgramFiles 'LibreOffice' +if (-not (Test-Path (Join-Path $officeSource 'program/soffice.exe'))) { throw 'LibreOffice installation was not found.' } +Copy-Item $officeSource (Join-Path $RuntimeRoot 'office') -Recurse +@' +Windows x64 native LibreOffice runtime from the libreoffice-fresh Chocolatey package. +https://community.chocolatey.org/packages/libreoffice-fresh +'@ | Set-Content -Path (Join-Path $RuntimeRoot 'office/SOURCE.txt') -Encoding UTF8 diff --git a/desktop/scripts/prepare.mjs b/desktop/scripts/prepare.mjs index 91cfc1e..6b4e99c 100644 --- a/desktop/scripts/prepare.mjs +++ b/desktop/scripts/prepare.mjs @@ -2,9 +2,8 @@ import { createHash } from 'node:crypto'; import { execFileSync } from 'node:child_process'; import { access, cp, mkdir, readFile, readdir, rename, rm, writeFile } from 'node:fs/promises'; import { homedir, tmpdir } from 'node:os'; -import { dirname, join, resolve } from 'node:path'; +import { basename, dirname, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { PNG } from 'pngjs'; const desktopRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..'); const repositoryRoot = resolve(desktopRoot, '..'); @@ -12,17 +11,27 @@ const tauriRoot = join(desktopRoot, 'src-tauri'); const resourcesRoot = join(tauriRoot, 'resources'); const cacheRoot = join(desktopRoot, '.cache'); const nodeVersion = 'v24.18.0'; -const nodeArchiveName = `node-${nodeVersion}-darwin-arm64.tar.gz`; +const supportedTargets = new Set(['darwin-arm64', 'win32-x64', 'linux-x64']); +const target = `${process.platform}-${process.arch}`; +const nodePlatform = process.platform === 'darwin' ? 'darwin' : process.platform === 'win32' ? 'win' : 'linux'; +const nodeExtension = process.platform === 'win32' ? 'zip' : 'tar.xz'; +const nodeArchiveName = `node-${nodeVersion}-${nodePlatform}-${process.arch}.${nodeExtension}`; const nodeDownloadBase = `https://nodejs.org/dist/${nodeVersion}`; function run(binary, args, options = {}) { execFileSync(binary, args, { cwd: options.cwd ?? repositoryRoot, env: { ...process.env, ...options.env }, - stdio: 'inherit' + stdio: options.stdio ?? 'inherit' }); } +function runNpm(args, options = {}) { + const npmCli = process.env.npm_execpath; + if (!npmCli) throw new Error('Run desktop staging through npm so npm_execpath is available.'); + run(process.execPath, [npmCli, ...args], options); +} + async function exists(path) { try { await access(path); @@ -34,17 +43,13 @@ async function exists(path) { async function download(url, destination) { const response = await fetch(url); - if (!response.ok || !response.body) { - throw new Error(`Could not download ${url}: HTTP ${response.status}`); - } + if (!response.ok || !response.body) throw new Error(`Could not download ${url}: HTTP ${response.status}`); await writeFile(destination, new Uint8Array(await response.arrayBuffer())); } async function verifyNodeArchive(archivePath) { const checksumPath = join(cacheRoot, `SHASUMS256-${nodeVersion}.txt`); - if (!(await exists(checksumPath))) { - await download(`${nodeDownloadBase}/SHASUMS256.txt`, checksumPath); - } + if (!(await exists(checksumPath))) await download(`${nodeDownloadBase}/SHASUMS256.txt`, checksumPath); const checksums = await readFile(checksumPath, 'utf8'); const checksumLine = checksums.split('\n').find((line) => line.endsWith(` ${nodeArchiveName}`)); if (!checksumLine) throw new Error(`Node checksum is missing for ${nodeArchiveName}.`); @@ -55,20 +60,18 @@ async function verifyNodeArchive(archivePath) { async function prepareNodeRuntime() { const archivePath = join(cacheRoot, nodeArchiveName); - if (!(await exists(archivePath))) { - await download(`${nodeDownloadBase}/${nodeArchiveName}`, archivePath); - } + if (!(await exists(archivePath))) await download(`${nodeDownloadBase}/${nodeArchiveName}`, archivePath); await verifyNodeArchive(archivePath); - - const extractionRoot = await mkdir(join(tmpdir(), `docuflex-node-${process.pid}`), { recursive: true }).then(() => - join(tmpdir(), `docuflex-node-${process.pid}`) - ); + const extractionRoot = join(tmpdir(), `docuflex-node-${process.pid}`); + await rm(extractionRoot, { recursive: true, force: true }); + await mkdir(extractionRoot, { recursive: true }); try { - run('tar', ['-xzf', archivePath, '-C', extractionRoot]); - const extractedNode = join(extractionRoot, `node-${nodeVersion}-darwin-arm64`); + run('tar', ['-xf', archivePath, '-C', extractionRoot]); + const extractedNode = join(extractionRoot, `node-${nodeVersion}-${nodePlatform}-${process.arch}`); const bundledNode = join(resourcesRoot, 'runtime', 'node'); await mkdir(join(bundledNode, 'bin'), { recursive: true }); - await cp(join(extractedNode, 'bin', 'node'), join(bundledNode, 'bin', 'node')); + const nodeName = process.platform === 'win32' ? 'node.exe' : 'node'; + await cp(join(extractedNode, nodeName === 'node.exe' ? nodeName : 'bin/node'), join(bundledNode, 'bin', nodeName)); await cp(join(extractedNode, 'LICENSE'), join(bundledNode, 'LICENSE')); } finally { await rm(extractionRoot, { recursive: true, force: true }); @@ -76,13 +79,18 @@ async function prepareNodeRuntime() { } function javaHome() { - return execFileSync('/usr/libexec/java_home', ['-v', '17'], { encoding: 'utf8' }).trim(); + if (process.env.JAVA_HOME?.trim()) return process.env.JAVA_HOME.trim(); + if (process.platform === 'darwin') { + return execFileSync('/usr/libexec/java_home', ['-v', '17'], { encoding: 'utf8' }).trim(); + } + throw new Error('JAVA_HOME must point to a JDK 17 runtime on Windows and Linux.'); } async function prepareJavaRuntime() { const jdk = javaHome(); const javaRuntime = join(resourcesRoot, 'runtime', 'java'); - run(join(jdk, 'bin', 'jlink'), [ + const jlink = join(jdk, 'bin', process.platform === 'win32' ? 'jlink.exe' : 'jlink'); + run(jlink, [ '--add-modules', 'java.base,java.desktop,java.naming,java.prefs,java.sql,jdk.httpserver', '--strip-debug', @@ -92,42 +100,62 @@ async function prepareJavaRuntime() { '--output', javaRuntime ]); - const legalDirectory = join(javaRuntime, 'legal'); - const expandedLegalDirectory = join(javaRuntime, 'legal-expanded'); - run('cp', ['-RL', legalDirectory, expandedLegalDirectory]); - await rm(legalDirectory, { recursive: true, force: true }); - await rename(expandedLegalDirectory, legalDirectory); - run('chmod', ['-R', 'u+rw', legalDirectory]); + if (process.platform === 'darwin') { + const legalDirectory = join(javaRuntime, 'legal'); + const expandedLegalDirectory = join(javaRuntime, 'legal-expanded'); + run('cp', ['-RL', legalDirectory, expandedLegalDirectory]); + await rm(legalDirectory, { recursive: true, force: true }); + await rename(expandedLegalDirectory, legalDirectory); + run('chmod', ['-R', 'u+rw', legalDirectory]); + } } async function preparePythonRuntime() { - const source = process.env.DOCUFLEX_PYTHON_RUNTIME?.trim() - || join(homedir(), '.cache/codex-runtimes/codex-primary-runtime/dependencies/python'); - const sourceLibrary = join(source, 'lib', 'python3.12'); - if (!(await exists(join(source, 'bin', 'python3.12'))) || !(await exists(sourceLibrary))) { - throw new Error('Set DOCUFLEX_PYTHON_RUNTIME to the arm64 Python 3.12 runtime used for desktop conversion.'); + const configured = process.env.DOCUFLEX_PYTHON_RUNTIME?.trim(); + const source = configured || (process.platform === 'darwin' + ? join(homedir(), '.cache/codex-runtimes/codex-primary-runtime/dependencies/python') + : ''); + if (!source || !(await exists(source))) { + throw new Error('DOCUFLEX_PYTHON_RUNTIME must point to a self-contained native Python 3.12 runtime.'); } - const destination = join(resourcesRoot, 'runtime', 'python'); - await mkdir(join(destination, 'bin'), { recursive: true }); - await mkdir(join(destination, 'lib'), { recursive: true }); - await cp(join(source, 'bin', 'python3.12'), join(destination, 'bin', 'python3.12')); - await cp(join(source, 'lib', 'libpython3.12.dylib'), join(destination, 'lib', 'libpython3.12.dylib')); - await cp(sourceLibrary, join(destination, 'lib', 'python3.12'), { - recursive: true, - filter: (path) => !path.startsWith(join(sourceLibrary, 'site-packages')) - }); + if (process.platform === 'darwin' && !configured) { + const sourceLibrary = join(source, 'lib', 'python3.12'); + await mkdir(join(destination, 'bin'), { recursive: true }); + await mkdir(join(destination, 'lib'), { recursive: true }); + await cp(join(source, 'bin', 'python3.12'), join(destination, 'bin', 'python3.12')); + await cp(join(source, 'lib', 'libpython3.12.dylib'), join(destination, 'lib', 'libpython3.12.dylib')); + await cp(sourceLibrary, join(destination, 'lib', 'python3.12'), { + recursive: true, + filter: (path) => !path.startsWith(join(sourceLibrary, 'site-packages')) + }); + const sitePackages = join(destination, 'lib', 'python3.12', 'site-packages'); + const sourcePackages = join(sourceLibrary, 'site-packages'); + await mkdir(sitePackages, { recursive: true }); + for (const packageName of ['PIL', 'docx', 'et_xmlfile', 'lxml', 'openpyxl', 'pptx', 'typing_extensions.py', 'xlsxwriter']) { + const packageSource = join(sourcePackages, packageName); + if (!(await exists(packageSource))) throw new Error(`Desktop conversion dependency is missing: ${packageName}`); + await cp(packageSource, join(sitePackages, packageName), { recursive: true }); + } + await writeFile(join(destination, 'bin', 'python3'), '#!/bin/sh\nexec "$(dirname "$0")/python3.12" "$@"\n'); + run('chmod', ['+x', join(destination, 'bin', 'python3'), join(destination, 'bin', 'python3.12')]); + } else { + await cp(source, destination, { recursive: true, dereference: process.platform === 'win32' }); + } + const executable = process.platform === 'win32' + ? join(destination, 'python.exe') + : join(destination, 'bin', 'python3'); + if (!(await exists(executable))) throw new Error(`Bundled Python executable is missing: ${executable}`); + run(executable, ['-c', 'import PIL, docx, lxml, openpyxl, pptx, xlsxwriter'], { cwd: destination }); +} - const sitePackages = join(destination, 'lib', 'python3.12', 'site-packages'); - const sourcePackages = join(sourceLibrary, 'site-packages'); - await mkdir(sitePackages, { recursive: true }); - for (const packageName of ['PIL', 'docx', 'et_xmlfile', 'lxml', 'openpyxl', 'pptx', 'typing_extensions.py', 'xlsxwriter']) { - const packageSource = join(sourcePackages, packageName); - if (!(await exists(packageSource))) throw new Error(`Desktop conversion dependency is missing: ${packageName}`); - await cp(packageSource, join(sitePackages, packageName), { recursive: true }); +async function copyRuntime(name, environmentName, fallback) { + const configured = process.env[environmentName]?.trim(); + const source = configured || fallback; + if (!source || !(await exists(source))) { + throw new Error(`${environmentName} must point to the prepared native ${name} runtime for ${target}.`); } - await writeFile(join(destination, 'bin', 'python3'), '#!/bin/sh\nexec "$(dirname "$0")/python3.12" "$@"\n'); - run('chmod', ['+x', join(destination, 'bin', 'python3'), join(destination, 'bin', 'python3.12')]); + await cp(source, join(resourcesRoot, 'runtime', name), { recursive: true, dereference: true }); } async function copyApplicationResources() { @@ -137,71 +165,81 @@ async function copyApplicationResources() { await cp(join(repositoryRoot, 'backend', 'fonts'), join(resourcesRoot, 'backend', 'fonts'), { recursive: true }); await mkdir(join(resourcesRoot, 'scripts'), { recursive: true }); for (const file of await readdir(join(repositoryRoot, 'scripts'))) { - if (!file.endsWith('.py')) continue; - await cp(join(repositoryRoot, 'scripts', file), join(resourcesRoot, 'scripts', file)); + if (file.endsWith('.py')) await cp(join(repositoryRoot, 'scripts', file), join(resourcesRoot, 'scripts', file)); } await mkdir(join(resourcesRoot, 'runtime'), { recursive: true }); - await cp(join(desktopRoot, 'runtime', 'supervise.sh'), join(resourcesRoot, 'runtime', 'supervise.sh')); - await cp(join(desktopRoot, 'runtime', 'soffice-shim.sh'), join(resourcesRoot, 'runtime', 'soffice-shim.sh')); - run('chmod', ['+x', join(resourcesRoot, 'runtime', 'soffice-shim.sh')]); - await cp( - join(desktopRoot, 'vendor', 'pdf2htmlEX-macos-arm64'), - join(resourcesRoot, 'runtime', 'pdf2htmlEX'), - { recursive: true } - ); - run('chmod', ['-R', 'u+w', join(resourcesRoot, 'runtime', 'pdf2htmlEX')]); - await cp( - join(desktopRoot, 'vendor', 'ocr-macos-arm64'), - join(resourcesRoot, 'runtime', 'ocr'), - { recursive: true } - ); - run('chmod', ['-R', 'u+w', join(resourcesRoot, 'runtime', 'ocr')]); + if (process.platform === 'win32') { + await cp(join(desktopRoot, 'runtime', 'supervise.ps1'), join(resourcesRoot, 'runtime', 'supervise.ps1')); + } else { + await cp(join(desktopRoot, 'runtime', 'supervise.sh'), join(resourcesRoot, 'runtime', 'supervise.sh')); + } + + const macPdfRuntime = process.platform === 'darwin' ? join(desktopRoot, 'vendor', 'pdf2htmlEX-macos-arm64') : ''; + const macOcrRuntime = process.platform === 'darwin' ? join(desktopRoot, 'vendor', 'ocr-macos-arm64') : ''; + await copyRuntime('pdf2htmlEX', 'DOCUFLEX_PDF2HTMLEX_RUNTIME', macPdfRuntime); + await copyRuntime('ocr', 'DOCUFLEX_OCR_RUNTIME', macOcrRuntime); + + if (process.platform === 'darwin' && !process.env.DOCUFLEX_OFFICE_RUNTIME?.trim()) { + const officeBin = join(resourcesRoot, 'runtime', 'office', 'bin'); + await mkdir(officeBin, { recursive: true }); + await cp(join(desktopRoot, 'runtime', 'soffice-shim.sh'), join(officeBin, 'soffice')); + } else { + await copyRuntime('office', 'DOCUFLEX_OFFICE_RUNTIME', ''); + } + + if (process.platform !== 'win32') { + run('chmod', ['-R', 'u+rwX', join(resourcesRoot, 'runtime')]); + for (const executable of [ + join(resourcesRoot, 'runtime', 'supervise.sh'), + join(resourcesRoot, 'runtime', 'office', 'bin', 'soffice'), + join(resourcesRoot, 'runtime', 'office', 'program', 'soffice'), + join(resourcesRoot, 'runtime', 'pdf2htmlEX', 'bin', 'pdf2htmlEX'), + join(resourcesRoot, 'runtime', 'ocr', 'bin', 'pdftoppm'), + join(resourcesRoot, 'runtime', 'ocr', 'bin', 'pdfunite'), + join(resourcesRoot, 'runtime', 'ocr', 'bin', 'tesseract') + ]) { + if (await exists(executable)) run('chmod', ['+x', executable]); + } + } } -async function generateMacIcon() { +async function generateIcons() { const source = join(repositoryRoot, 'public', 'macos-icon-iOS-Default-1024x1024@1x.png'); - const iconset = join(cacheRoot, 'Docuflex.iconset'); const iconRoot = join(tauriRoot, 'icons'); - await rm(iconset, { recursive: true, force: true }); - await mkdir(iconset, { recursive: true }); - await mkdir(iconRoot, { recursive: true }); - const parsedIcon = PNG.sync.read(await readFile(source)); - await writeFile(join(iconRoot, 'icon.png'), PNG.sync.write(parsedIcon, { colorType: 6, bitDepth: 8 })); - - const variants = [ - [16, 'icon_16x16.png'], - [32, 'icon_16x16@2x.png'], - [32, 'icon_32x32.png'], - [64, 'icon_32x32@2x.png'], - [128, 'icon_128x128.png'], - [256, 'icon_128x128@2x.png'], - [256, 'icon_256x256.png'], - [512, 'icon_256x256@2x.png'], - [512, 'icon_512x512.png'], - [1024, 'icon_512x512@2x.png'] - ]; - for (const [size, name] of variants) { - run('sips', ['-z', String(size), String(size), source, '--out', join(iconset, name)]); - } - run('iconutil', ['-c', 'icns', iconset, '-o', join(iconRoot, 'icon.icns')]); + await rm(iconRoot, { recursive: true, force: true }); + const tauriCli = join(desktopRoot, 'node_modules', '@tauri-apps', 'cli', 'tauri.js'); + run(process.execPath, [tauriCli, 'icon', source, '--output', iconRoot], { cwd: desktopRoot }); +} + +async function writeRuntimeManifest() { + const manifest = { + target, + generatedAt: new Date().toISOString(), + node: nodeVersion, + java: '17 (jlink)', + python: '3.12', + pdf2htmlEX: process.platform === 'win32' ? '0.14.6 native static' : '0.18.8.rc1', + ocr: 'Tesseract with eng, deu, and osd data', + offline: true + }; + await writeFile(join(resourcesRoot, 'RUNTIME.json'), `${JSON.stringify(manifest, null, 2)}\n`); } async function main() { - if (process.platform !== 'darwin' || process.arch !== 'arm64') { - throw new Error('This desktop package currently targets Apple Silicon macOS.'); - } + if (!supportedTargets.has(target)) throw new Error(`Unsupported desktop build target: ${target}`); await mkdir(cacheRoot, { recursive: true }); await rm(resourcesRoot, { recursive: true, force: true }); await rm(join(tauriRoot, 'target', 'release', 'resources'), { recursive: true, force: true }); await mkdir(resourcesRoot, { recursive: true }); - run('npm', ['run', 'build']); - run('npm', ['run', 'backend:compile']); + runNpm(['run', 'build']); + runNpm(['run', 'backend:compile']); await copyApplicationResources(); await prepareNodeRuntime(); await prepareJavaRuntime(); await preparePythonRuntime(); - await generateMacIcon(); + await generateIcons(); + await writeRuntimeManifest(); } await main(); diff --git a/desktop/scripts/smoke-runtimes.mjs b/desktop/scripts/smoke-runtimes.mjs new file mode 100644 index 0000000..390e347 --- /dev/null +++ b/desktop/scripts/smoke-runtimes.mjs @@ -0,0 +1,95 @@ +import { execFileSync } from 'node:child_process'; +import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join, resolve } from 'node:path'; + +const desktopRoot = resolve(import.meta.dirname, '..'); +const resources = join(desktopRoot, 'src-tauri', 'resources'); +const windows = process.platform === 'win32'; +const executable = (runtime, name) => join(resources, 'runtime', runtime, 'bin', `${name}${windows ? '.exe' : ''}`); +const ocrExecutable = (name) => windows && name !== 'tesseract' + ? join(resources, 'runtime', 'ocr', 'poppler', 'bin', `${name}.exe`) + : executable('ocr', name); +const python = windows ? join(resources, 'runtime', 'python', 'python.exe') : executable('python', 'python3'); +const office = process.platform === 'win32' + ? join(resources, 'runtime', 'office', 'program', 'soffice.exe') + : process.platform === 'linux' + ? join(resources, 'runtime', 'office', 'program', 'soffice') + : join(resources, 'runtime', 'office', 'bin', 'soffice'); + +function run(binary, args, options = {}) { + return execFileSync(binary, args, { + cwd: options.cwd ?? resources, + encoding: options.encoding ?? 'utf8', + env: { ...process.env, ...options.env }, + stdio: options.stdio ?? ['ignore', 'pipe', 'pipe'], + timeout: options.timeout ?? 30_000 + }); +} + +function minimalPdf(text) { + const objects = [ + '<< /Type /Catalog /Pages 2 0 R >>', + '<< /Type /Pages /Kids [3 0 R] /Count 1 >>', + '<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Resources << /Font << /F1 5 0 R >> >> /Contents 4 0 R >>', + `<< /Length ${text.length + 31} >>\nstream\nBT /F1 24 Tf 72 720 Td (${text}) Tj ET\nendstream`, + '<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>' + ]; + let pdf = '%PDF-1.4\n'; + const offsets = [0]; + objects.forEach((object, index) => { + offsets.push(Buffer.byteLength(pdf)); + pdf += `${index + 1} 0 obj\n${object}\nendobj\n`; + }); + const xref = Buffer.byteLength(pdf); + pdf += `xref\n0 ${objects.length + 1}\n0000000000 65535 f \n`; + pdf += offsets.slice(1).map((offset) => `${String(offset).padStart(10, '0')} 00000 n \n`).join(''); + pdf += `trailer\n<< /Size ${objects.length + 1} /Root 1 0 R >>\nstartxref\n${xref}\n%%EOF\n`; + return pdf; +} + +const temporary = await mkdtemp(join(tmpdir(), 'docuflex-runtime-smoke-')); +try { + run(executable('node', 'node'), ['--version']); + run(executable('java', 'java'), ['--version']); + run(python, ['-c', 'import PIL, docx, lxml, openpyxl, pptx, xlsxwriter; print("python-ok")']); + run(ocrExecutable('pdftoppm'), ['-v']); + run(ocrExecutable('pdfunite'), ['-v']); + const languages = run(ocrExecutable('tesseract'), ['--list-langs'], { + env: { TESSDATA_PREFIX: join(resources, 'runtime', 'ocr', 'share', 'tessdata') } + }); + for (const language of ['eng', 'deu', 'osd']) { + if (!languages.split(/\s+/).includes(language)) throw new Error(`OCR language is missing: ${language}`); + } + + const image = join(temporary, 'ocr.png'); + const font = join(resources, 'backend', 'fonts', 'inter-variable-normal.ttf'); + run(python, ['-c', [ + 'from PIL import Image, ImageDraw, ImageFont', + `image=Image.new("RGB",(1200,260),"white")`, + `font=ImageFont.truetype(${JSON.stringify(font)},96)`, + 'ImageDraw.Draw(image).text((35,55),"DOCUFLEX OFFLINE",font=font,fill="black")', + `image.save(${JSON.stringify(image)})` + ].join(';')]); + const ocrText = run(ocrExecutable('tesseract'), [image, 'stdout', '-l', 'eng', '--psm', '7'], { + env: { TESSDATA_PREFIX: join(resources, 'runtime', 'ocr', 'share', 'tessdata') }, + timeout: 60_000 + }); + if (!/docuflex/i.test(ocrText)) throw new Error(`Bundled OCR smoke test failed: ${ocrText.trim()}`); + + const pdf = join(temporary, 'document.pdf'); + const html = join(temporary, 'document.html'); + await writeFile(pdf, minimalPdf('Docuflex Offline')); + run(executable('pdf2htmlEX', 'pdf2htmlEX'), [ + '--data-dir', join(resources, 'runtime', 'pdf2htmlEX', 'share', 'pdf2htmlEX'), + '--quiet', '1', '--embed', '1', '--correct-text-visibility', '0', + '--dest-dir', temporary, pdf, 'document.html' + ], { timeout: 120_000 }); + const converted = await readFile(html, 'utf8'); + if (!/Docuflex/.test(converted)) throw new Error('Bundled pdf2htmlEX smoke test did not preserve text.'); + + if (process.platform !== 'darwin') run(office, ['--headless', '--version'], { timeout: 60_000 }); + process.stdout.write(`Docuflex ${process.platform}-${process.arch} offline runtime smoke test passed.\n`); +} finally { + await rm(temporary, { recursive: true, force: true }); +} diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock index 7ce04d5..4580b69 100644 --- a/desktop/src-tauri/Cargo.lock +++ b/desktop/src-tauri/Cargo.lock @@ -745,6 +745,15 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "dlib" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" +dependencies = [ + "libloading", +] + [[package]] name = "dlopen2" version = "0.8.2" @@ -773,6 +782,7 @@ name = "docuflex-desktop" version = "0.0.1" dependencies = [ "libc", + "rfd", "tauri", "tauri-build", "tauri-plugin-single-instance", @@ -794,6 +804,12 @@ dependencies = [ "tendril", ] +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + [[package]] name = "dpi" version = "0.1.2" @@ -2417,7 +2433,7 @@ checksum = "7da1d65da6dd5d1e44199ac0f58712d241c0f439f80adea8924d832384087f85" dependencies = [ "base64 0.22.1", "indexmap 2.14.0", - "quick-xml", + "quick-xml 0.41.0", "serde", "time", ] @@ -2462,6 +2478,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "pollster" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" + [[package]] name = "potential_utf" version = "0.1.5" @@ -2545,6 +2567,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quick-xml" +version = "0.39.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" +dependencies = [ + "memchr", +] + [[package]] name = "quick-xml" version = "0.41.0" @@ -2684,6 +2715,33 @@ dependencies = [ "web-sys", ] +[[package]] +name = "rfd" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20dafead71c16a34e1ff357ddefc8afc11e7d51d6d2b9fbd07eaa48e3e540220" +dependencies = [ + "block2", + "dispatch2", + "js-sys", + "libc", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "percent-encoding", + "pollster", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "web-sys", + "windows-sys 0.61.2", +] + [[package]] name = "rustc-hash" version = "2.1.3" @@ -2778,6 +2836,12 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + [[package]] name = "scopeguard" version = "1.2.0" @@ -4088,6 +4152,66 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wayland-backend" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2857dd20b54e916ec7253b3d6b4d5c4d7d4ca2c33c2e11c6c76a99bd8744755d" +dependencies = [ + "cc", + "downcast-rs", + "rustix", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c7c96bb74690c3189b5c9cb4ca1627062bb23693a4fad9d8c3de958260144" +dependencies = [ + "bitflags 2.13.1", + "rustix", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d0c813de3daa2ed6520af85a3bd49b0e722a3078506899aa9686fea58dc4b6" +dependencies = [ + "bitflags 2.13.1", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c324a910fd86ebdc364a3e61ec1f11737d3b1d6c273c0239ee8ff4bc0d24b4a" +dependencies = [ + "proc-macro2", + "quick-xml 0.39.4", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" +dependencies = [ + "dlib", + "log", + "pkg-config", +] + [[package]] name = "web-sys" version = "0.3.103" diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml index f0837bd..71da4ab 100644 --- a/desktop/src-tauri/Cargo.toml +++ b/desktop/src-tauri/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "docuflex-desktop" version = "0.0.1" -description = "Docuflex PDF editor for macOS" +description = "Docuflex offline PDF editor for macOS, Windows, and Linux" authors = ["Maximilian Bayer"] edition = "2021" @@ -14,6 +14,7 @@ tauri-build = { version = "2.5.6", features = [] } [dependencies] libc = "0.2.186" +rfd = "0.17.2" tauri = { version = "2.11.5", features = [] } tauri-plugin-single-instance = "2.4.0" url = "2.5.7" diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index 0a1a4b7..59656d9 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -1,4 +1,5 @@ use std::{ + ffi::OsString, fs::{self, File}, io::{Read, Write}, net::{SocketAddr, TcpStream}, @@ -8,10 +9,9 @@ use std::{ thread, time::{Duration, Instant}, }; -use tauri::{ - webview::DownloadEvent, LogicalPosition, Manager, TitleBarStyle, WebviewUrl, - WebviewWindowBuilder, -}; +use tauri::{webview::DownloadEvent, Manager, WebviewUrl, WebviewWindowBuilder}; +#[cfg(target_os = "macos")] +use tauri::{LogicalPosition, TitleBarStyle}; use url::Url; const FRONTEND_PORT: u16 = 43_127; @@ -29,6 +29,14 @@ impl Services { unsafe { libc::kill(child.id() as i32, libc::SIGTERM); } + #[cfg(target_os = "windows")] + { + let _ = Command::new("taskkill.exe") + .args(["/PID", &child.id().to_string(), "/T", "/F"]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status(); + } } for child in children.iter_mut() { let _ = child.wait(); @@ -38,6 +46,83 @@ impl Services { } } +fn runtime_executable(resource_root: &Path, runtime: &str, name: &str) -> PathBuf { + #[cfg(target_os = "windows")] + let executable = format!("{name}.exe"); + #[cfg(not(target_os = "windows"))] + let executable = name.to_string(); + resource_root + .join("runtime") + .join(runtime) + .join("bin") + .join(executable) +} + +fn python_executable(resource_root: &Path) -> PathBuf { + #[cfg(target_os = "windows")] + return resource_root.join("runtime/python/python.exe"); + #[cfg(not(target_os = "windows"))] + return resource_root.join("runtime/python/bin/python3"); +} + +fn office_executable(resource_root: &Path) -> PathBuf { + #[cfg(target_os = "windows")] + return resource_root.join("runtime/office/program/soffice.exe"); + #[cfg(target_os = "linux")] + return resource_root.join("runtime/office/program/soffice"); + #[cfg(target_os = "macos")] + return resource_root.join("runtime/office/bin/soffice"); +} + +fn ocr_executable(resource_root: &Path, name: &str) -> PathBuf { + #[cfg(target_os = "windows")] + { + if name != "tesseract" { + return resource_root + .join("runtime/ocr/poppler/bin") + .join(format!("{name}.exe")); + } + } + runtime_executable(resource_root, "ocr", name) +} + +fn supervised_command( + resource_root: &Path, + parent_pid: &str, + executable: &Path, + arguments: &[OsString], +) -> Command { + #[cfg(target_os = "windows")] + { + let mut command = Command::new("powershell.exe"); + command + .arg("-NoLogo") + .arg("-NoProfile") + .arg("-NonInteractive") + .arg("-ExecutionPolicy") + .arg("Bypass") + .arg("-File") + .arg(resource_root.join("runtime/supervise.ps1")) + .arg("-DocuflexParentPid") + .arg(parent_pid) + .arg("-FilePath") + .arg(executable) + .arg("--") + .args(arguments); + command + } + #[cfg(not(target_os = "windows"))] + { + let mut command = Command::new("/bin/sh"); + command + .arg(resource_root.join("runtime/supervise.sh")) + .arg(parent_pid) + .arg(executable) + .args(arguments); + command + } +} + impl Drop for Services { fn drop(&mut self) { self.stop(); @@ -58,22 +143,27 @@ fn spawn_services( resource_root: &Path, log_directory: &Path, ) -> Result, Box> { - let java = resource_root.join("runtime/java/bin/java"); - let node = resource_root.join("runtime/node/bin/node"); + let java = runtime_executable(resource_root, "java", "java"); + let node = runtime_executable(resource_root, "node", "node"); let pdf2html = resource_root.join("runtime/pdf2htmlEX"); let ocr = resource_root.join("runtime/ocr"); - let python = resource_root.join("runtime/python/bin/python3"); - let document_converter = resource_root.join("runtime/soffice-shim.sh"); - let supervisor = resource_root.join("runtime/supervise.sh"); + let python = python_executable(resource_root); + let document_converter = office_executable(resource_root); let parent_pid = std::process::id().to_string(); + #[cfg(target_os = "windows")] + let classpath = "backend/out;backend/lib/pdfbox-app-3.0.8.jar"; + #[cfg(not(target_os = "windows"))] let classpath = "backend/out:backend/lib/pdfbox-app-3.0.8.jar"; + let backend_arguments = [ + OsString::from("-cp"), + OsString::from(classpath), + OsString::from("DocuflexPdfServer"), + ]; let (backend_stdout, backend_stderr) = log_file(log_directory, "pdf-backend")?; - let backend = Command::new("/bin/sh") + let mut backend_command = + supervised_command(resource_root, &parent_pid, &java, &backend_arguments); + let backend = backend_command .current_dir(resource_root) - .arg(&supervisor) - .arg(&parent_pid) - .arg(java) - .args(["-cp", classpath, "DocuflexPdfServer"]) .env("PDF_BACKEND_HOST", "127.0.0.1") .env("PDF_BACKEND_PORT", BACKEND_PORT.to_string()) .env( @@ -85,13 +175,12 @@ fn spawn_services( .stderr(backend_stderr) .spawn()?; + let frontend_arguments = [OsString::from("frontend/index.js")]; let (frontend_stdout, frontend_stderr) = log_file(log_directory, "frontend")?; - let frontend = Command::new("/bin/sh") + let mut frontend_command = + supervised_command(resource_root, &parent_pid, &node, &frontend_arguments); + frontend_command .current_dir(resource_root) - .arg(&supervisor) - .arg(&parent_pid) - .arg(node) - .arg("frontend/index.js") .env("HOST", "127.0.0.1") .env("PORT", FRONTEND_PORT.to_string()) .env("ORIGIN", format!("http://127.0.0.1:{FRONTEND_PORT}")) @@ -103,18 +192,23 @@ fn spawn_services( .env("ADDRESS_HEADER", "") .env("PROTOCOL_HEADER", "") .env("HOST_HEADER", "") - .env("PDF2HTMLEX_BIN", pdf2html.join("bin/pdf2htmlEX")) + .env( + "PDF2HTMLEX_BIN", + runtime_executable(resource_root, "pdf2htmlEX", "pdf2htmlEX"), + ) .env("PDF2HTMLEX_DATA_DIR", pdf2html.join("share/pdf2htmlEX")) - .env("FONTCONFIG_PATH", pdf2html.join("etc/fonts")) - .env("FONTCONFIG_FILE", "fonts.conf") - .env("PDFTOPPM_BIN", ocr.join("bin/pdftoppm")) - .env("PDFUNITE_BIN", ocr.join("bin/pdfunite")) - .env("TESSERACT_BIN", ocr.join("bin/tesseract")) + .env("PDFTOPPM_BIN", ocr_executable(resource_root, "pdftoppm")) + .env("PDFUNITE_BIN", ocr_executable(resource_root, "pdfunite")) + .env("TESSERACT_BIN", ocr_executable(resource_root, "tesseract")) .env("TESSDATA_PREFIX", ocr.join("share/tessdata")) - .env("PDF_RENDER_BIN", ocr.join("bin/pdftoppm")) + .env("PDF_RENDER_BIN", ocr_executable(resource_root, "pdftoppm")) .env("DOCUMENT_CONVERTER_PYTHON", python) - .env("DOCUMENT_CONVERTER_BIN", document_converter) - .env("PATH", "/usr/bin:/bin:/usr/sbin:/sbin") + .env("DOCUMENT_CONVERTER_BIN", document_converter); + #[cfg(target_os = "macos")] + frontend_command + .env("FONTCONFIG_PATH", pdf2html.join("etc/fonts")) + .env("FONTCONFIG_FILE", "fonts.conf"); + let frontend = frontend_command .stdin(Stdio::null()) .stdout(frontend_stdout) .stderr(frontend_stderr) @@ -158,29 +252,9 @@ fn choose_download_destination(suggested: &Path) -> Option { .and_then(|name| name.to_str()) .filter(|name| !name.is_empty()) .unwrap_or("document.pdf"); - let script = r#" -on run argv - set suggestedName to item 1 of argv - set chosenFile to choose file name with prompt "Save exported document" default name suggestedName - return POSIX path of chosenFile -end run -"#; - let output = Command::new("/usr/bin/osascript") - .arg("-e") - .arg(script) - .arg(suggested_name) - .output() - .ok()?; - if !output.status.success() { - return None; - } - let selected = String::from_utf8(output.stdout).ok()?; - let selected = selected.trim(); - if selected.is_empty() { - None - } else { - Some(PathBuf::from(selected)) - } + rfd::FileDialog::new() + .set_file_name(suggested_name) + .save_file() } fn editor_initialization_script() -> &'static str { @@ -221,19 +295,49 @@ pub fn run() { let editor_url = Url::parse(&format!("http://127.0.0.1:{FRONTEND_PORT}/editor"))?; let allowed_origin = editor_url.origin().ascii_serialization(); - WebviewWindowBuilder::new(app, "main", WebviewUrl::External(editor_url)) - .title("Docuflex") + let window_actions = app.handle().clone(); + let window_builder = + WebviewWindowBuilder::new(app, "main", WebviewUrl::External(editor_url)) + .title("Docuflex") + .inner_size(1440.0, 920.0) + .min_inner_size(960.0, 640.0) + .center(); + #[cfg(target_os = "macos")] + let window_builder = window_builder .title_bar_style(TitleBarStyle::Overlay) .hidden_title(true) - .traffic_light_position(LogicalPosition::new(24.0, 24.0)) - .inner_size(1440.0, 920.0) - .min_inner_size(960.0, 640.0) - .center() + .traffic_light_position(LogicalPosition::new(24.0, 24.0)); + #[cfg(target_os = "windows")] + let window_builder = window_builder.decorations(false).shadow(true); + window_builder .initialization_script(editor_initialization_script()) .on_navigation(move |url| { if url.scheme() == "about" { return true; } + if url.origin().ascii_serialization() == allowed_origin { + if let Some(action) = url.path().strip_prefix("/__docuflex/window/") { + if let Some(window) = window_actions.get_webview_window("main") { + match action { + "minimize" => { + let _ = window.minimize(); + } + "maximize" => { + if window.is_maximized().unwrap_or(false) { + let _ = window.unmaximize(); + } else { + let _ = window.maximize(); + } + } + "close" => { + let _ = window.close(); + } + _ => {} + } + } + return false; + } + } url.origin().ascii_serialization() == allowed_origin && url.path() == "/editor" }) .on_download(|_webview, event| match event { diff --git a/desktop/src-tauri/tauri.conf.json b/desktop/src-tauri/tauri.conf.json index efcfcb5..481fe6b 100644 --- a/desktop/src-tauri/tauri.conf.json +++ b/desktop/src-tauri/tauri.conf.json @@ -14,13 +14,21 @@ }, "bundle": { "active": true, - "targets": ["app"], - "icon": ["icons/icon.png", "icons/icon.icns"], + "targets": "all", + "icon": ["icons/32x32.png", "icons/128x128.png", "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico"], "resources": ["resources/**/*"], "macOS": { "minimumSystemVersion": "26.0", "signingIdentity": "-", "entitlements": "Entitlements.plist" + }, + "windows": { + "webviewInstallMode": { + "type": "offlineInstaller" + }, + "nsis": { + "installMode": "currentUser" + } } } } From 8ef8e2e362b544eeb5799c90792839da8b1162f0 Mon Sep 17 00:00:00 2001 From: max06bayer <101212752+max06bayer@users.noreply.github.com> Date: Sat, 18 Jul 2026 14:31:01 +0200 Subject: [PATCH 02/32] Bundle Linux LibreOffice symlink targets --- desktop/scripts/prepare-linux-runtimes.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/desktop/scripts/prepare-linux-runtimes.sh b/desktop/scripts/prepare-linux-runtimes.sh index 8e0d4ae..efe9097 100644 --- a/desktop/scripts/prepare-linux-runtimes.sh +++ b/desktop/scripts/prepare-linux-runtimes.sh @@ -68,7 +68,9 @@ for package in poppler-utils tesseract-ocr tesseract-ocr-eng tesseract-ocr-deu t fi done -cp -a /usr/lib/libreoffice/. "$RUNTIME_ROOT/office/" +# Dereference Ubuntu's links into /usr/share/libreoffice so the packaged +# runtime remains self-contained after it leaves the build machine. +cp -aL /usr/lib/libreoffice/. "$RUNTIME_ROOT/office/" chmod +x "$RUNTIME_ROOT/office/program/soffice" "$RUNTIME_ROOT/office/program/soffice.bin" cat > "$RUNTIME_ROOT/ocr/SOURCE.txt" <<'EOF' From 5c35136b84f7cea8f5912fbf743ee7eb17956972 Mon Sep 17 00:00:00 2001 From: max06bayer <101212752+max06bayer@users.noreply.github.com> Date: Sat, 18 Jul 2026 14:35:27 +0200 Subject: [PATCH 03/32] Generate the Tauri desktop shell during staging --- desktop/scripts/prepare.mjs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/desktop/scripts/prepare.mjs b/desktop/scripts/prepare.mjs index 6b4e99c..111732e 100644 --- a/desktop/scripts/prepare.mjs +++ b/desktop/scripts/prepare.mjs @@ -8,6 +8,7 @@ import { fileURLToPath } from 'node:url'; const desktopRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..'); const repositoryRoot = resolve(desktopRoot, '..'); const tauriRoot = join(desktopRoot, 'src-tauri'); +const distRoot = join(desktopRoot, 'dist'); const resourcesRoot = join(tauriRoot, 'resources'); const cacheRoot = join(desktopRoot, '.cache'); const nodeVersion = 'v24.18.0'; @@ -225,6 +226,21 @@ async function writeRuntimeManifest() { await writeFile(join(resourcesRoot, 'RUNTIME.json'), `${JSON.stringify(manifest, null, 2)}\n`); } +async function writeDesktopShell() { + await rm(distRoot, { recursive: true, force: true }); + await mkdir(distRoot, { recursive: true }); + await writeFile(join(distRoot, 'index.html'), ` + + + + + Docuflex + + + +`); +} + async function main() { if (!supportedTargets.has(target)) throw new Error(`Unsupported desktop build target: ${target}`); await mkdir(cacheRoot, { recursive: true }); @@ -240,6 +256,7 @@ async function main() { await preparePythonRuntime(); await generateIcons(); await writeRuntimeManifest(); + await writeDesktopShell(); } await main(); From df2e639000aa206eef2c099901a32cd0dd133ef3 Mon Sep 17 00:00:00 2001 From: max06bayer <101212752+max06bayer@users.noreply.github.com> Date: Sat, 18 Jul 2026 14:37:43 +0200 Subject: [PATCH 04/32] Normalize legacy Windows pdf2htmlEX arguments --- desktop/runtime/pdf2htmlEX-windows-wrapper.rs | 30 +++++++++++++++++++ desktop/scripts/prepare-windows-runtimes.ps1 | 8 +++-- 2 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 desktop/runtime/pdf2htmlEX-windows-wrapper.rs diff --git a/desktop/runtime/pdf2htmlEX-windows-wrapper.rs b/desktop/runtime/pdf2htmlEX-windows-wrapper.rs new file mode 100644 index 0000000..7e7e1c0 --- /dev/null +++ b/desktop/runtime/pdf2htmlEX-windows-wrapper.rs @@ -0,0 +1,30 @@ +use std::env; +use std::path::PathBuf; +use std::process::{exit, Command}; + +fn main() { + let executable = env::current_exe().expect("Could not locate the pdf2htmlEX launcher"); + let native = executable + .parent() + .map(|directory| directory.join("pdf2htmlEX-native.exe")) + .unwrap_or_else(|| PathBuf::from("pdf2htmlEX-native.exe")); + + let mut normalized = Vec::new(); + let mut arguments = env::args_os().skip(1); + while let Some(argument) = arguments.next() { + let option = argument.to_string_lossy(); + if option == "--quiet" || option == "--embed" { + let _ = arguments.next(); + continue; + } + normalized.push(argument); + } + + match Command::new(native).args(normalized).status() { + Ok(status) => exit(status.code().unwrap_or(1)), + Err(error) => { + eprintln!("Could not start bundled pdf2htmlEX: {error}"); + exit(1); + } + } +} diff --git a/desktop/scripts/prepare-windows-runtimes.ps1 b/desktop/scripts/prepare-windows-runtimes.ps1 index 2d50f05..ec97b51 100644 --- a/desktop/scripts/prepare-windows-runtimes.ps1 +++ b/desktop/scripts/prepare-windows-runtimes.ps1 @@ -26,11 +26,15 @@ $pdfExtract = Join-Path $RuntimeRoot 'pdf2htmlEX-extract' Expand-Archive -LiteralPath $pdfArchive -DestinationPath $pdfExtract $pdfRuntime = Join-Path $RuntimeRoot 'pdf2htmlEX' New-Item -ItemType Directory -Force -Path (Join-Path $pdfRuntime 'bin'), (Join-Path $pdfRuntime 'share') | Out-Null -Copy-Item (Join-Path $pdfExtract 'pdf2htmlEX.exe') (Join-Path $pdfRuntime 'bin/pdf2htmlEX.exe') +Copy-Item (Join-Path $pdfExtract 'pdf2htmlEX.exe') (Join-Path $pdfRuntime 'bin/pdf2htmlEX-native.exe') +$pdfWrapperSource = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '../runtime/pdf2htmlEX-windows-wrapper.rs')) +& rustc $pdfWrapperSource -C opt-level=z -C strip=symbols -o (Join-Path $pdfRuntime 'bin/pdf2htmlEX.exe') +if ($LASTEXITCODE -ne 0) { throw 'Could not compile the native Windows pdf2htmlEX launcher.' } Copy-Item (Join-Path $pdfExtract 'data') (Join-Path $pdfRuntime 'share/pdf2htmlEX') -Recurse Copy-Item (Join-Path $pdfExtract 'LICENSE*') $pdfRuntime @' -Windows native static pdf2htmlEX 0.14.6 with poppler-data. +Windows native static pdf2htmlEX 0.14.6 with poppler-data and a native launcher +that normalizes the newer endpoint CLI shorthands for this legacy build. Source and binary distribution: https://soft.rubypdf.com/software/pdf2htmlex-windows-version Archive SHA-256: e92aa55699c3e9d9b4b4954bea157e59b5c3363cbe9a7713495c553544026354 '@ | Set-Content -Path (Join-Path $pdfRuntime 'SOURCE.txt') -Encoding UTF8 From 16441ac2588a605f54df6223e4addb31e3a87582 Mon Sep 17 00:00:00 2001 From: max06bayer <101212752+max06bayer@users.noreply.github.com> Date: Sat, 18 Jul 2026 14:46:36 +0200 Subject: [PATCH 05/32] Preserve bundled Linux runtime binaries --- desktop/scripts/build.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/desktop/scripts/build.mjs b/desktop/scripts/build.mjs index 363cede..86448cc 100644 --- a/desktop/scripts/build.mjs +++ b/desktop/scripts/build.mjs @@ -10,8 +10,8 @@ const bundles = process.platform === 'darwin' ? ['nsis'] : ['appimage', 'deb']; -execFileSync(process.execPath, [tauriCli, 'build', '--bundles', bundles.join(',')], { +execFileSync(process.execPath, [tauriCli, 'build', '--verbose', '--bundles', bundles.join(',')], { cwd: desktopRoot, - env: process.env, + env: process.platform === 'linux' ? { ...process.env, NO_STRIP: '1' } : process.env, stdio: 'inherit' }); From 52e469d345a7f6c4bc70d8b601b0d38b9fa6f0ce Mon Sep 17 00:00:00 2001 From: max06bayer <101212752+max06bayer@users.noreply.github.com> Date: Sat, 18 Jul 2026 14:47:24 +0200 Subject: [PATCH 06/32] Use the Windows LibreOffice console launcher --- desktop/scripts/prepare-windows-runtimes.ps1 | 2 +- desktop/scripts/smoke-runtimes.mjs | 2 +- desktop/src-tauri/src/lib.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/desktop/scripts/prepare-windows-runtimes.ps1 b/desktop/scripts/prepare-windows-runtimes.ps1 index ec97b51..4c7f53a 100644 --- a/desktop/scripts/prepare-windows-runtimes.ps1 +++ b/desktop/scripts/prepare-windows-runtimes.ps1 @@ -76,7 +76,7 @@ Tesseract fast language data 4.1.0: https://github.com/tesseract-ocr/tessdata_fa choco install libreoffice-fresh --yes --no-progress $officeSource = Join-Path $env:ProgramFiles 'LibreOffice' -if (-not (Test-Path (Join-Path $officeSource 'program/soffice.exe'))) { throw 'LibreOffice installation was not found.' } +if (-not (Test-Path (Join-Path $officeSource 'program/soffice.com'))) { throw 'LibreOffice console launcher was not found.' } Copy-Item $officeSource (Join-Path $RuntimeRoot 'office') -Recurse @' Windows x64 native LibreOffice runtime from the libreoffice-fresh Chocolatey package. diff --git a/desktop/scripts/smoke-runtimes.mjs b/desktop/scripts/smoke-runtimes.mjs index 390e347..1a9db92 100644 --- a/desktop/scripts/smoke-runtimes.mjs +++ b/desktop/scripts/smoke-runtimes.mjs @@ -12,7 +12,7 @@ const ocrExecutable = (name) => windows && name !== 'tesseract' : executable('ocr', name); const python = windows ? join(resources, 'runtime', 'python', 'python.exe') : executable('python', 'python3'); const office = process.platform === 'win32' - ? join(resources, 'runtime', 'office', 'program', 'soffice.exe') + ? join(resources, 'runtime', 'office', 'program', 'soffice.com') : process.platform === 'linux' ? join(resources, 'runtime', 'office', 'program', 'soffice') : join(resources, 'runtime', 'office', 'bin', 'soffice'); diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index 59656d9..0de567b 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -67,7 +67,7 @@ fn python_executable(resource_root: &Path) -> PathBuf { fn office_executable(resource_root: &Path) -> PathBuf { #[cfg(target_os = "windows")] - return resource_root.join("runtime/office/program/soffice.exe"); + return resource_root.join("runtime/office/program/soffice.com"); #[cfg(target_os = "linux")] return resource_root.join("runtime/office/program/soffice"); #[cfg(target_os = "macos")] From 60127e687392e27260d928db4c9db7432dee0cd5 Mon Sep 17 00:00:00 2001 From: max06bayer <101212752+max06bayer@users.noreply.github.com> Date: Sat, 18 Jul 2026 14:58:43 +0200 Subject: [PATCH 07/32] Expose the bundled JVM to Linux packaging --- desktop/scripts/build.mjs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/desktop/scripts/build.mjs b/desktop/scripts/build.mjs index 86448cc..869d452 100644 --- a/desktop/scripts/build.mjs +++ b/desktop/scripts/build.mjs @@ -9,9 +9,19 @@ const bundles = process.platform === 'darwin' : process.platform === 'win32' ? ['nsis'] : ['appimage', 'deb']; +const buildEnvironment = process.platform === 'linux' + ? { + ...process.env, + NO_STRIP: '1', + LD_LIBRARY_PATH: [ + join(desktopRoot, 'src-tauri', 'resources', 'runtime', 'java', 'lib', 'server'), + process.env.LD_LIBRARY_PATH + ].filter(Boolean).join(':') + } + : process.env; execFileSync(process.execPath, [tauriCli, 'build', '--verbose', '--bundles', bundles.join(',')], { cwd: desktopRoot, - env: process.platform === 'linux' ? { ...process.env, NO_STRIP: '1' } : process.env, + env: buildEnvironment, stdio: 'inherit' }); From 64c16e4a708b537e23ccfff96a347ed01163aa35 Mon Sep 17 00:00:00 2001 From: max06bayer <101212752+max06bayer@users.noreply.github.com> Date: Sat, 18 Jul 2026 16:14:28 +0200 Subject: [PATCH 08/32] Fix AppImage service startup on rolling Linux --- .github/workflows/desktop-native-builds.yml | 31 ++++++++++++- desktop/src-tauri/src/lib.rs | 48 +++++++++++++++++++-- 2 files changed, 74 insertions(+), 5 deletions(-) diff --git a/.github/workflows/desktop-native-builds.yml b/.github/workflows/desktop-native-builds.yml index 280493e..66b8936 100644 --- a/.github/workflows/desktop-native-builds.yml +++ b/.github/workflows/desktop-native-builds.yml @@ -75,7 +75,7 @@ jobs: - name: Install Tauri Linux build dependencies run: | sudo apt-get update - sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libfuse2 + sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libfuse2 xvfb - uses: actions/setup-node@v4 with: node-version: 24.18.0 @@ -109,6 +109,35 @@ jobs: DOCUFLEX_OFFICE_RUNTIME: ${{ github.workspace }}/desktop/.native-runtime/linux/office DOCUFLEX_PYTHON_RUNTIME: ${{ env.pythonLocation }} run: npm run build + - name: Launch-test packaged AppImage + working-directory: desktop + run: | + appimage=$(find src-tauri/target/release/bundle/appimage -maxdepth 1 -name '*.AppImage' -print -quit) + test -n "$appimage" + chmod +x "$appimage" + timeout 75s xvfb-run -a "$appimage" > /tmp/docuflex-appimage.log 2>&1 & + launcher_pid=$! + cleanup() { + kill "$launcher_pid" 2>/dev/null || true + wait "$launcher_pid" 2>/dev/null || true + } + trap cleanup EXIT + for attempt in $(seq 1 60); do + if ! kill -0 "$launcher_pid" 2>/dev/null; then + cat /tmp/docuflex-appimage.log + echo "Packaged AppImage exited before its local services became ready." >&2 + exit 1 + fi + if curl --fail --silent http://127.0.0.1:43128/health >/dev/null \ + && curl --fail --silent http://127.0.0.1:43127/editor >/dev/null; then + echo "Packaged AppImage local-service launch test passed." + exit 0 + fi + sleep 1 + done + cat /tmp/docuflex-appimage.log + echo "Packaged AppImage local services were not ready within 60 seconds." >&2 + exit 1 - name: Upload Linux packages uses: actions/upload-artifact@v4 with: diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index 0de567b..e6c9d59 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -123,6 +123,17 @@ fn supervised_command( } } +fn isolate_service_environment(_command: &mut Command) { + #[cfg(target_os = "linux")] + { + // AppImage's AppRun prepends its Ubuntu libraries so the GUI can load. + // Those paths must not leak into our self-contained Java and Node + // runtimes on rolling distributions such as Arch/CachyOS. + _command.env_remove("LD_LIBRARY_PATH"); + _command.env_remove("LD_PRELOAD"); + } +} + impl Drop for Services { fn drop(&mut self) { self.stop(); @@ -162,6 +173,7 @@ fn spawn_services( let (backend_stdout, backend_stderr) = log_file(log_directory, "pdf-backend")?; let mut backend_command = supervised_command(resource_root, &parent_pid, &java, &backend_arguments); + isolate_service_environment(&mut backend_command); let backend = backend_command .current_dir(resource_root) .env("PDF_BACKEND_HOST", "127.0.0.1") @@ -179,6 +191,7 @@ fn spawn_services( let (frontend_stdout, frontend_stderr) = log_file(log_directory, "frontend")?; let mut frontend_command = supervised_command(resource_root, &parent_pid, &node, &frontend_arguments); + isolate_service_environment(&mut frontend_command); frontend_command .current_dir(resource_root) .env("HOST", "127.0.0.1") @@ -235,15 +248,41 @@ fn service_ready(port: u16, path: &str) -> bool { response[..size].starts_with(b"HTTP/1.1 200") } -fn wait_for_services() -> Result<(), Box> { - let deadline = Instant::now() + Duration::from_secs(20); +fn log_tail(path: &Path) -> String { + let Ok(contents) = fs::read_to_string(path) else { + return "log unavailable".to_string(); + }; + let mut tail = contents.chars().rev().take(4_000).collect::>(); + tail.reverse(); + let tail = tail.into_iter().collect::(); + if tail.trim().is_empty() { + "log is empty".to_string() + } else { + tail + } +} + +fn service_startup_error(log_directory: &Path) -> String { + let backend = log_tail(&log_directory.join("pdf-backend.log")); + let frontend = log_tail(&log_directory.join("frontend.log")); + format!( + "Docuflex local services did not start within 60 seconds.\n\ + Logs: {}\n\n[pdf-backend]\n{}\n\n[frontend]\n{}", + log_directory.display(), + backend, + frontend + ) +} + +fn wait_for_services(log_directory: &Path) -> Result<(), Box> { + let deadline = Instant::now() + Duration::from_secs(60); while Instant::now() < deadline { if service_ready(BACKEND_PORT, "/health") && service_ready(FRONTEND_PORT, "/editor") { return Ok(()); } thread::sleep(Duration::from_millis(120)); } - Err("Docuflex local services did not start within 20 seconds.".into()) + Err(service_startup_error(log_directory).into()) } fn choose_download_destination(suggested: &Path) -> Option { @@ -288,7 +327,8 @@ pub fn run() { .lock() .map_err(|_| "Could not track local services.")? = children; - if let Err(error) = wait_for_services() { + if let Err(error) = wait_for_services(&log_directory) { + eprintln!("{error}"); services_for_setup.stop(); return Err(error); } From 34e4b1befca468d796a623e98babbbb0951773ca Mon Sep 17 00:00:00 2001 From: max06bayer <101212752+max06bayer@users.noreply.github.com> Date: Sat, 18 Jul 2026 16:57:36 +0200 Subject: [PATCH 09/32] Use compatible WebKit rendering on Linux --- .github/workflows/desktop-native-builds.yml | 9 ++++--- desktop/src-tauri/src/lib.rs | 29 +++++++++++++++++++++ 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/.github/workflows/desktop-native-builds.yml b/.github/workflows/desktop-native-builds.yml index 66b8936..149ca03 100644 --- a/.github/workflows/desktop-native-builds.yml +++ b/.github/workflows/desktop-native-builds.yml @@ -115,7 +115,9 @@ jobs: appimage=$(find src-tauri/target/release/bundle/appimage -maxdepth 1 -name '*.AppImage' -print -quit) test -n "$appimage" chmod +x "$appimage" - timeout 75s xvfb-run -a "$appimage" > /tmp/docuflex-appimage.log 2>&1 & + rm -f /tmp/docuflex-page-loaded + DOCUFLEX_PAGE_LOAD_MARKER=/tmp/docuflex-page-loaded \ + timeout 75s xvfb-run -a "$appimage" > /tmp/docuflex-appimage.log 2>&1 & launcher_pid=$! cleanup() { kill "$launcher_pid" 2>/dev/null || true @@ -129,8 +131,9 @@ jobs: exit 1 fi if curl --fail --silent http://127.0.0.1:43128/health >/dev/null \ - && curl --fail --silent http://127.0.0.1:43127/editor >/dev/null; then - echo "Packaged AppImage local-service launch test passed." + && curl --fail --silent http://127.0.0.1:43127/editor >/dev/null \ + && grep --quiet '^editor-loaded$' /tmp/docuflex-page-loaded 2>/dev/null; then + echo "Packaged AppImage service and WebKit page-load test passed." exit 0 fi sleep 1 diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index e6c9d59..8348255 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -9,6 +9,8 @@ use std::{ thread, time::{Duration, Instant}, }; +#[cfg(target_os = "linux")] +use tauri::webview::PageLoadEvent; use tauri::{webview::DownloadEvent, Manager, WebviewUrl, WebviewWindowBuilder}; #[cfg(target_os = "macos")] use tauri::{LogicalPosition, TitleBarStyle}; @@ -17,6 +19,22 @@ use url::Url; const FRONTEND_PORT: u16 = 43_127; const BACKEND_PORT: u16 = 43_128; +fn configure_platform_webview() { + #[cfg(target_os = "linux")] + { + // WebKitGTK accelerated compositing can abort its web process while + // creating an EGL display on Arch/CachyOS, especially under Wayland + // and NVIDIA. Preserve explicit user overrides, otherwise use the + // broadly compatible software-rendering path for this editor shell. + if std::env::var_os("WEBKIT_DISABLE_DMABUF_RENDERER").is_none() { + std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1"); + } + if std::env::var_os("WEBKIT_DISABLE_COMPOSITING_MODE").is_none() { + std::env::set_var("WEBKIT_DISABLE_COMPOSITING_MODE", "1"); + } + } +} + struct Services { children: Mutex>, } @@ -302,6 +320,7 @@ fn editor_initialization_script() -> &'static str { #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { + configure_platform_webview(); let services = Arc::new(Services { children: Mutex::new(Vec::new()), }); @@ -336,6 +355,8 @@ pub fn run() { let editor_url = Url::parse(&format!("http://127.0.0.1:{FRONTEND_PORT}/editor"))?; let allowed_origin = editor_url.origin().ascii_serialization(); let window_actions = app.handle().clone(); + #[cfg(target_os = "linux")] + let page_load_marker = std::env::var_os("DOCUFLEX_PAGE_LOAD_MARKER").map(PathBuf::from); let window_builder = WebviewWindowBuilder::new(app, "main", WebviewUrl::External(editor_url)) .title("Docuflex") @@ -349,6 +370,14 @@ pub fn run() { .traffic_light_position(LogicalPosition::new(24.0, 24.0)); #[cfg(target_os = "windows")] let window_builder = window_builder.decorations(false).shadow(true); + #[cfg(target_os = "linux")] + let window_builder = window_builder.on_page_load(move |_window, payload| { + if payload.event() == PageLoadEvent::Finished && payload.url().path() == "/editor" { + if let Some(marker) = &page_load_marker { + let _ = fs::write(marker, b"editor-loaded\n"); + } + } + }); window_builder .initialization_script(editor_initialization_script()) .on_navigation(move |url| { From 09e77949e40f4c78aaf4b9190f5030ff3621c1b8 Mon Sep 17 00:00:00 2001 From: max06bayer <101212752+max06bayer@users.noreply.github.com> Date: Sat, 18 Jul 2026 17:33:38 +0200 Subject: [PATCH 10/32] Set WebKit compatibility flags in Linux launcher --- .github/workflows/desktop-native-builds.yml | 5 ++++- desktop/runtime/docuflex.desktop.hbs | 8 ++++++++ desktop/runtime/linux-app-launcher.sh | 15 +++++++++++++++ desktop/src-tauri/tauri.linux.conf.json | 17 +++++++++++++++++ 4 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 desktop/runtime/docuflex.desktop.hbs create mode 100755 desktop/runtime/linux-app-launcher.sh create mode 100644 desktop/src-tauri/tauri.linux.conf.json diff --git a/.github/workflows/desktop-native-builds.yml b/.github/workflows/desktop-native-builds.yml index 149ca03..d768122 100644 --- a/.github/workflows/desktop-native-builds.yml +++ b/.github/workflows/desktop-native-builds.yml @@ -115,7 +115,8 @@ jobs: appimage=$(find src-tauri/target/release/bundle/appimage -maxdepth 1 -name '*.AppImage' -print -quit) test -n "$appimage" chmod +x "$appimage" - rm -f /tmp/docuflex-page-loaded + rm -f /tmp/docuflex-launcher-env /tmp/docuflex-page-loaded + DOCUFLEX_LAUNCHER_MARKER=/tmp/docuflex-launcher-env \ DOCUFLEX_PAGE_LOAD_MARKER=/tmp/docuflex-page-loaded \ timeout 75s xvfb-run -a "$appimage" > /tmp/docuflex-appimage.log 2>&1 & launcher_pid=$! @@ -132,6 +133,8 @@ jobs: fi if curl --fail --silent http://127.0.0.1:43128/health >/dev/null \ && curl --fail --silent http://127.0.0.1:43127/editor >/dev/null \ + && grep --quiet '^dmabuf=1$' /tmp/docuflex-launcher-env 2>/dev/null \ + && grep --quiet '^compositing=1$' /tmp/docuflex-launcher-env 2>/dev/null \ && grep --quiet '^editor-loaded$' /tmp/docuflex-page-loaded 2>/dev/null; then echo "Packaged AppImage service and WebKit page-load test passed." exit 0 diff --git a/desktop/runtime/docuflex.desktop.hbs b/desktop/runtime/docuflex.desktop.hbs new file mode 100644 index 0000000..8a514e7 --- /dev/null +++ b/desktop/runtime/docuflex.desktop.hbs @@ -0,0 +1,8 @@ +[Desktop Entry] +Categories={{categories}} +{{#if comment}}Comment={{comment}}{{/if}} +Exec=docuflex-launcher %U +Icon={{icon}} +Name={{name}} +Terminal=false +Type=Application diff --git a/desktop/runtime/linux-app-launcher.sh b/desktop/runtime/linux-app-launcher.sh new file mode 100755 index 0000000..802dba2 --- /dev/null +++ b/desktop/runtime/linux-app-launcher.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +# WebKitGTK reads these during shared-library initialization, before the +# Tauri/Rust entry point can run. Keep them in this outer launcher. +: "${WEBKIT_DISABLE_DMABUF_RENDERER:=1}" +: "${WEBKIT_DISABLE_COMPOSITING_MODE:=1}" +export WEBKIT_DISABLE_DMABUF_RENDERER WEBKIT_DISABLE_COMPOSITING_MODE + +if [ -n "${DOCUFLEX_LAUNCHER_MARKER:-}" ]; then + printf 'dmabuf=%s\ncompositing=%s\n' \ + "$WEBKIT_DISABLE_DMABUF_RENDERER" \ + "$WEBKIT_DISABLE_COMPOSITING_MODE" > "$DOCUFLEX_LAUNCHER_MARKER" +fi + +exec docuflex-desktop "$@" diff --git a/desktop/src-tauri/tauri.linux.conf.json b/desktop/src-tauri/tauri.linux.conf.json new file mode 100644 index 0000000..0d5aaf8 --- /dev/null +++ b/desktop/src-tauri/tauri.linux.conf.json @@ -0,0 +1,17 @@ +{ + "bundle": { + "linux": { + "appimage": { + "files": { + "/usr/bin/docuflex-launcher": "../runtime/linux-app-launcher.sh" + } + }, + "deb": { + "desktopTemplate": "../runtime/docuflex.desktop.hbs", + "files": { + "/usr/bin/docuflex-launcher": "../runtime/linux-app-launcher.sh" + } + } + } + } +} From 370ca9f61c8b7459c35b554a0c0500ca23c79d3e Mon Sep 17 00:00:00 2001 From: max06bayer <101212752+max06bayer@users.noreply.github.com> Date: Sat, 18 Jul 2026 18:00:29 +0200 Subject: [PATCH 11/32] Replace Linux shell launcher with native executable --- desktop/runtime/linux-app-launcher.c | 57 +++++++++++++++++++++++ desktop/runtime/linux-app-launcher.sh | 15 ------ desktop/scripts/prepare-linux-runtimes.sh | 5 ++ desktop/src-tauri/tauri.linux.conf.json | 4 +- 4 files changed, 64 insertions(+), 17 deletions(-) create mode 100644 desktop/runtime/linux-app-launcher.c delete mode 100755 desktop/runtime/linux-app-launcher.sh diff --git a/desktop/runtime/linux-app-launcher.c b/desktop/runtime/linux-app-launcher.c new file mode 100644 index 0000000..0293a4b --- /dev/null +++ b/desktop/runtime/linux-app-launcher.c @@ -0,0 +1,57 @@ +#define _POSIX_C_SOURCE 200809L + +#include +#include +#include +#include +#include + +static int set_default_environment(const char *name, const char *value) { + if (getenv(name) != NULL) { + return 0; + } + return setenv(name, value, 1); +} + +static void write_test_marker(void) { + const char *marker = getenv("DOCUFLEX_LAUNCHER_MARKER"); + if (marker == NULL || marker[0] == '\0') { + return; + } + + FILE *output = fopen(marker, "w"); + if (output == NULL) { + return; + } + fprintf(output, "dmabuf=%s\ncompositing=%s\n", + getenv("WEBKIT_DISABLE_DMABUF_RENDERER"), + getenv("WEBKIT_DISABLE_COMPOSITING_MODE")); + fclose(output); +} + +int main(int argc, char **argv) { + if (set_default_environment("WEBKIT_DISABLE_DMABUF_RENDERER", "1") != 0 || + set_default_environment("WEBKIT_DISABLE_COMPOSITING_MODE", "1") != 0) { + fprintf(stderr, "Docuflex launcher could not configure WebKit: %s\n", + strerror(errno)); + return 126; + } + + write_test_marker(); + + char **child_arguments = calloc((size_t)argc + 1, sizeof(char *)); + if (child_arguments == NULL) { + fprintf(stderr, "Docuflex launcher could not allocate arguments.\n"); + return 126; + } + child_arguments[0] = (char *)"docuflex-desktop"; + for (int index = 1; index < argc; index++) { + child_arguments[index] = argv[index]; + } + + execvp(child_arguments[0], child_arguments); + fprintf(stderr, "Docuflex launcher could not start the application: %s\n", + strerror(errno)); + free(child_arguments); + return 127; +} diff --git a/desktop/runtime/linux-app-launcher.sh b/desktop/runtime/linux-app-launcher.sh deleted file mode 100755 index 802dba2..0000000 --- a/desktop/runtime/linux-app-launcher.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/sh - -# WebKitGTK reads these during shared-library initialization, before the -# Tauri/Rust entry point can run. Keep them in this outer launcher. -: "${WEBKIT_DISABLE_DMABUF_RENDERER:=1}" -: "${WEBKIT_DISABLE_COMPOSITING_MODE:=1}" -export WEBKIT_DISABLE_DMABUF_RENDERER WEBKIT_DISABLE_COMPOSITING_MODE - -if [ -n "${DOCUFLEX_LAUNCHER_MARKER:-}" ]; then - printf 'dmabuf=%s\ncompositing=%s\n' \ - "$WEBKIT_DISABLE_DMABUF_RENDERER" \ - "$WEBKIT_DISABLE_COMPOSITING_MODE" > "$DOCUFLEX_LAUNCHER_MARKER" -fi - -exec docuflex-desktop "$@" diff --git a/desktop/scripts/prepare-linux-runtimes.sh b/desktop/scripts/prepare-linux-runtimes.sh index efe9097..9cde536 100644 --- a/desktop/scripts/prepare-linux-runtimes.sh +++ b/desktop/scripts/prepare-linux-runtimes.sh @@ -17,6 +17,11 @@ mkdir -p "$RUNTIME_ROOT/pdf2htmlEX/bin" "$RUNTIME_ROOT/pdf2htmlEX/share" \ "$RUNTIME_ROOT/ocr/bin" "$RUNTIME_ROOT/ocr/lib" "$RUNTIME_ROOT/ocr/share/tessdata" \ "$RUNTIME_ROOT/office" +cc -O2 -Wall -Wextra -Werror \ + desktop/runtime/linux-app-launcher.c \ + -o "$RUNTIME_ROOT/docuflex-launcher" +chmod +x "$RUNTIME_ROOT/docuflex-launcher" + PDF2HTMLEX_URL=https://github.com/pdf2htmlEX/pdf2htmlEX/releases/download/v0.18.8.rc1/pdf2htmlEX-0.18.8.rc1-master-20200630-Ubuntu-focal-x86_64.AppImage PDF2HTMLEX_SHA256=11de2583a3abce5f141fd7fafb1fea2c67b15886e546d6b7675c600012e6ab8c PDF2HTMLEX_IMAGE="$RUNTIME_ROOT/pdf2htmlEX.AppImage" diff --git a/desktop/src-tauri/tauri.linux.conf.json b/desktop/src-tauri/tauri.linux.conf.json index 0d5aaf8..7a44155 100644 --- a/desktop/src-tauri/tauri.linux.conf.json +++ b/desktop/src-tauri/tauri.linux.conf.json @@ -3,13 +3,13 @@ "linux": { "appimage": { "files": { - "/usr/bin/docuflex-launcher": "../runtime/linux-app-launcher.sh" + "/usr/bin/docuflex-launcher": "../.native-runtime/linux/docuflex-launcher" } }, "deb": { "desktopTemplate": "../runtime/docuflex.desktop.hbs", "files": { - "/usr/bin/docuflex-launcher": "../runtime/linux-app-launcher.sh" + "/usr/bin/docuflex-launcher": "../.native-runtime/linux/docuflex-launcher" } } } From 062883761f4e32800bbfe7867033968c4ca9d499 Mon Sep 17 00:00:00 2001 From: max06bayer <101212752+max06bayer@users.noreply.github.com> Date: Sat, 18 Jul 2026 18:41:24 +0200 Subject: [PATCH 12/32] Build native Arch and CachyOS package --- .github/workflows/desktop-native-builds.yml | 74 +++++++++++++++++++++ desktop/scripts/package-arch-linux.sh | 70 +++++++++++++++++++ desktop/src-tauri/src/lib.rs | 18 ++--- 3 files changed, 153 insertions(+), 9 deletions(-) create mode 100644 desktop/scripts/package-arch-linux.sh diff --git a/.github/workflows/desktop-native-builds.yml b/.github/workflows/desktop-native-builds.yml index d768122..5f8d5e2 100644 --- a/.github/workflows/desktop-native-builds.yml +++ b/.github/workflows/desktop-native-builds.yml @@ -152,3 +152,77 @@ jobs: path: | desktop/src-tauri/target/release/bundle/appimage/*.AppImage desktop/src-tauri/target/release/bundle/deb/*.deb + + arch-linux-x64: + name: Arch and CachyOS native x64 package + needs: linux-x64 + runs-on: ubuntu-22.04 + container: archlinux:latest + timeout-minutes: 90 + steps: + - name: Install current Arch build and runtime dependencies + run: | + pacman -Syu --noconfirm + pacman -S --noconfirm --needed \ + base-devel binutils curl dbus git gtk3 libayatana-appindicator \ + librsvg nodejs npm rust webkit2gtk-4.1 xorg-server-xvfb + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: + name: Docuflex-Linux-x64 + path: desktop/.linux-bundle + - name: Extract offline application resources + run: | + deb=$(find desktop/.linux-bundle -name '*.deb' -print -quit) + test -n "$deb" + mkdir -p desktop/.deb-extracted desktop/.deb-parts + (cd desktop/.deb-parts && ar x "$GITHUB_WORKSPACE/$deb") + data_archive=$(find desktop/.deb-parts -name 'data.tar.*' -print -quit) + test -n "$data_archive" + bsdtar -xf "$data_archive" -C desktop/.deb-extracted + - name: Build current-Arch native package + run: | + chmod +x desktop/scripts/package-arch-linux.sh + desktop/scripts/package-arch-linux.sh \ + "$GITHUB_WORKSPACE/desktop/.deb-extracted" \ + "$GITHUB_WORKSPACE/desktop/.arch-package" + - name: Install and launch-test native package + run: | + package=$(find desktop/.arch-package -name '*.pkg.tar.zst' -print -quit) + test -n "$package" + pacman -U --noconfirm "$package" + rm -f /tmp/docuflex-page-loaded + DOCUFLEX_PAGE_LOAD_MARKER=/tmp/docuflex-page-loaded \ + timeout 75s xvfb-run -a dbus-run-session -- docuflex-desktop \ + > /tmp/docuflex-arch.log 2>&1 & + app_pid=$! + cleanup() { + kill "$app_pid" 2>/dev/null || true + wait "$app_pid" 2>/dev/null || true + } + trap cleanup EXIT + for attempt in $(seq 1 60); do + if ! kill -0 "$app_pid" 2>/dev/null; then + cat /tmp/docuflex-arch.log + echo 'Native Arch package exited before startup completed.' >&2 + exit 1 + fi + if curl --fail --silent http://127.0.0.1:43128/health >/dev/null \ + && curl --fail --silent http://127.0.0.1:43127/editor >/dev/null \ + && grep --quiet '^editor-loaded$' /tmp/docuflex-page-loaded 2>/dev/null; then + echo 'Native Arch services and WebKit page-load test passed.' + exit 0 + fi + sleep 1 + done + cat /tmp/docuflex-arch.log + echo 'Native Arch package did not become ready within 60 seconds.' >&2 + exit 1 + - name: Upload Arch and CachyOS package + uses: actions/upload-artifact@v4 + with: + name: Docuflex-Arch-CachyOS-x64 + if-no-files-found: error + path: | + desktop/.arch-package/*.pkg.tar.zst + desktop/.arch-package/docuflex-arch-ldd.txt diff --git a/desktop/scripts/package-arch-linux.sh b/desktop/scripts/package-arch-linux.sh new file mode 100644 index 0000000..0413eb1 --- /dev/null +++ b/desktop/scripts/package-arch-linux.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +set -euo pipefail + +DEB_ROOT=${1:?Pass the extracted Debian package root.} +OUTPUT_DIR=${2:?Pass the package output directory.} +REPOSITORY_ROOT=$(cd "$(dirname "$0")/../.." && pwd) +DESKTOP_ROOT="$REPOSITORY_ROOT/desktop" +TAURI_ROOT="$DESKTOP_ROOT/src-tauri" +PACKAGE_ROOT=$(mktemp -d) +trap 'rm -rf -- "$PACKAGE_ROOT"' EXIT + +RESOURCE_SOURCE=$(find "$DEB_ROOT/usr/lib" -mindepth 1 -maxdepth 1 -type d -print -quit) +test -n "$RESOURCE_SOURCE" +RESOURCE_NAME=$(basename "$RESOURCE_SOURCE") + +rm -rf -- "$TAURI_ROOT/resources" "$DESKTOP_ROOT/dist" +mkdir -p "$TAURI_ROOT/resources" "$DESKTOP_ROOT/dist" +cp -a "$RESOURCE_SOURCE/." "$TAURI_ROOT/resources/" +printf '%s\n' '' > "$DESKTOP_ROOT/dist/index.html" + +npm ci --prefix "$DESKTOP_ROOT" +node "$DESKTOP_ROOT/node_modules/@tauri-apps/cli/tauri.js" icon \ + "$REPOSITORY_ROOT/public/macos-icon-iOS-Default-1024x1024@1x.png" \ + --output "$TAURI_ROOT/icons" +cargo build --release --manifest-path "$TAURI_ROOT/Cargo.toml" + +install -Dm755 "$TAURI_ROOT/target/release/docuflex-desktop" \ + "$PACKAGE_ROOT/usr/bin/docuflex-desktop" +mkdir -p "$PACKAGE_ROOT/usr/lib/$RESOURCE_NAME" +cp -a "$RESOURCE_SOURCE/." "$PACKAGE_ROOT/usr/lib/$RESOURCE_NAME/" +install -Dm644 "$TAURI_ROOT/icons/128x128.png" \ + "$PACKAGE_ROOT/usr/share/icons/hicolor/128x128/apps/docuflex.png" +install -Dm644 /dev/stdin "$PACKAGE_ROOT/usr/share/applications/docuflex.desktop" <<'EOF' +[Desktop Entry] +Categories=Office; +Comment=Offline PDF editor +Exec=docuflex-desktop %U +Icon=docuflex +Name=Docuflex +Terminal=false +Type=Application +EOF + +PACKAGE_SIZE=$(du -sk "$PACKAGE_ROOT/usr" | awk '{print $1 * 1024}') +cat > "$PACKAGE_ROOT/.PKGINFO" <&2 + exit 1 +fi diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index 8348255..161f93f 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -22,15 +22,15 @@ const BACKEND_PORT: u16 = 43_128; fn configure_platform_webview() { #[cfg(target_os = "linux")] { - // WebKitGTK accelerated compositing can abort its web process while - // creating an EGL display on Arch/CachyOS, especially under Wayland - // and NVIDIA. Preserve explicit user overrides, otherwise use the - // broadly compatible software-rendering path for this editor shell. - if std::env::var_os("WEBKIT_DISABLE_DMABUF_RENDERER").is_none() { - std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1"); - } - if std::env::var_os("WEBKIT_DISABLE_COMPOSITING_MODE").is_none() { - std::env::set_var("WEBKIT_DISABLE_COMPOSITING_MODE", "1"); + // The AppImage launcher must set these before WebKitGTK is loaded. + // Native distro packages use the host WebKit/Mesa stack unmodified. + if std::env::var_os("APPIMAGE").is_some() { + if std::env::var_os("WEBKIT_DISABLE_DMABUF_RENDERER").is_none() { + std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1"); + } + if std::env::var_os("WEBKIT_DISABLE_COMPOSITING_MODE").is_none() { + std::env::set_var("WEBKIT_DISABLE_COMPOSITING_MODE", "1"); + } } } } From 55d1cebb6f59a7adbbddaa7780616c50e2d414ad Mon Sep 17 00:00:00 2001 From: max06bayer <101212752+max06bayer@users.noreply.github.com> Date: Sat, 18 Jul 2026 19:05:52 +0200 Subject: [PATCH 13/32] Upload native Arch package artifact --- .github/workflows/desktop-native-builds.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/desktop-native-builds.yml b/.github/workflows/desktop-native-builds.yml index 5f8d5e2..a1780bb 100644 --- a/.github/workflows/desktop-native-builds.yml +++ b/.github/workflows/desktop-native-builds.yml @@ -185,10 +185,10 @@ jobs: chmod +x desktop/scripts/package-arch-linux.sh desktop/scripts/package-arch-linux.sh \ "$GITHUB_WORKSPACE/desktop/.deb-extracted" \ - "$GITHUB_WORKSPACE/desktop/.arch-package" + "$GITHUB_WORKSPACE/desktop/arch-package" - name: Install and launch-test native package run: | - package=$(find desktop/.arch-package -name '*.pkg.tar.zst' -print -quit) + package=$(find desktop/arch-package -name '*.pkg.tar.zst' -print -quit) test -n "$package" pacman -U --noconfirm "$package" rm -f /tmp/docuflex-page-loaded @@ -224,5 +224,5 @@ jobs: name: Docuflex-Arch-CachyOS-x64 if-no-files-found: error path: | - desktop/.arch-package/*.pkg.tar.zst - desktop/.arch-package/docuflex-arch-ldd.txt + desktop/arch-package/*.pkg.tar.zst + desktop/arch-package/docuflex-arch-ldd.txt From 565ee401a0a74a32fa3a502d17777ec1a0052330 Mon Sep 17 00:00:00 2001 From: max06bayer <101212752+max06bayer@users.noreply.github.com> Date: Sat, 18 Jul 2026 20:43:16 +0200 Subject: [PATCH 14/32] Fix Linux editor viewport and Windows startup --- .github/workflows/desktop-native-builds.yml | 46 ++++++++++++++++++ desktop/runtime/chrome.js | 8 ++++ desktop/src-tauri/src/lib.rs | 52 +++++++++++++-------- desktop/src-tauri/src/main.rs | 2 + 4 files changed, 88 insertions(+), 20 deletions(-) diff --git a/.github/workflows/desktop-native-builds.yml b/.github/workflows/desktop-native-builds.yml index a1780bb..869117b 100644 --- a/.github/workflows/desktop-native-builds.yml +++ b/.github/workflows/desktop-native-builds.yml @@ -59,6 +59,52 @@ jobs: run: | $env:DOCUFLEX_PYTHON_RUNTIME = $env:pythonLocation npm run build + - name: Launch-test packaged Windows runtime + shell: pwsh + working-directory: desktop + run: | + $runtimeRoot = Resolve-Path 'src-tauri/resources' + $releaseRoot = Resolve-Path 'src-tauri/target/release' + $releaseResources = Join-Path $releaseRoot 'resources' + if (Test-Path $releaseResources) { + Remove-Item -LiteralPath $releaseResources -Recurse -Force + } + Copy-Item -LiteralPath $runtimeRoot -Destination $releaseResources -Recurse + $marker = Join-Path $env:RUNNER_TEMP 'docuflex-windows-page-loaded' + Remove-Item -LiteralPath $marker -Force -ErrorAction SilentlyContinue + $env:DOCUFLEX_PAGE_LOAD_MARKER = $marker + $process = Start-Process -FilePath (Join-Path $releaseRoot 'docuflex-desktop.exe') -PassThru + try { + foreach ($attempt in 1..60) { + $process.Refresh() + if ($process.HasExited) { + throw "Packaged Windows app exited with code $($process.ExitCode) before startup completed." + } + $backendReady = $false + $frontendReady = $false + try { + $backendReady = (Invoke-WebRequest -UseBasicParsing 'http://127.0.0.1:43128/health' -TimeoutSec 2).StatusCode -eq 200 + $frontendReady = (Invoke-WebRequest -UseBasicParsing 'http://127.0.0.1:43127/editor' -TimeoutSec 2).StatusCode -eq 200 + } catch {} + if ($backendReady -and $frontendReady -and (Test-Path $marker) -and ((Get-Content $marker -Raw).Trim() -eq 'editor-loaded')) { + Write-Host 'Packaged Windows services and WebView page-load test passed.' + exit 0 + } + Start-Sleep -Seconds 1 + } + throw 'Packaged Windows app did not become ready within 60 seconds.' + } finally { + if (-not $process.HasExited) { + & taskkill.exe /PID $process.Id /T /F | Out-Null + } + $logRoot = Join-Path $env:LOCALAPPDATA 'com.docuflex.editor/logs' + if (Test-Path $logRoot) { + Get-ChildItem $logRoot -Filter '*.log' | ForEach-Object { + Write-Host "[$($_.Name)]" + Get-Content $_.FullName + } + } + } - name: Upload Windows installer uses: actions/upload-artifact@v4 with: diff --git a/desktop/runtime/chrome.js b/desktop/runtime/chrome.js index b029ae0..ca46814 100644 --- a/desktop/runtime/chrome.js +++ b/desktop/runtime/chrome.js @@ -254,6 +254,14 @@ position: absolute; } + html[data-docuflex-desktop="linux"] .editor-shell { + height: 117.6470588dvh !important; + transform: scale(0.85); + transform-origin: top left; + width: 117.6470588vw !important; + zoom: 1 !important; + } + html[data-docuflex-desktop="windows"] .topbar, html[data-docuflex-desktop="windows"] .brand-area, html[data-docuflex-desktop="windows"] .tab-strip, diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index 161f93f..a606881 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -9,7 +9,7 @@ use std::{ thread, time::{Duration, Instant}, }; -#[cfg(target_os = "linux")] +#[cfg(any(target_os = "linux", target_os = "windows"))] use tauri::webview::PageLoadEvent; use tauri::{webview::DownloadEvent, Manager, WebviewUrl, WebviewWindowBuilder}; #[cfg(target_os = "macos")] @@ -112,21 +112,12 @@ fn supervised_command( ) -> Command { #[cfg(target_os = "windows")] { - let mut command = Command::new("powershell.exe"); - command - .arg("-NoLogo") - .arg("-NoProfile") - .arg("-NonInteractive") - .arg("-ExecutionPolicy") - .arg("Bypass") - .arg("-File") - .arg(resource_root.join("runtime/supervise.ps1")) - .arg("-DocuflexParentPid") - .arg(parent_pid) - .arg("-FilePath") - .arg(executable) - .arg("--") - .args(arguments); + use std::os::windows::process::CommandExt; + + const CREATE_NO_WINDOW: u32 = 0x0800_0000; + let _ = (resource_root, parent_pid); + let mut command = Command::new(executable); + command.args(arguments).creation_flags(CREATE_NO_WINDOW); command } #[cfg(not(target_os = "windows"))] @@ -303,6 +294,21 @@ fn wait_for_services(log_directory: &Path) -> Result<(), Box Option { let suggested_name = suggested .file_name() @@ -340,14 +346,20 @@ pub fn run() { .setup(move |app| { let resource_root = app.path().resource_dir()?.join("resources"); let log_directory = app.path().app_log_dir()?; - let children = spawn_services(&resource_root, &log_directory)?; + let children = match spawn_services(&resource_root, &log_directory) { + Ok(children) => children, + Err(error) => { + report_startup_error(&log_directory, error.as_ref()); + return Err(error); + } + }; *services_for_setup .children .lock() .map_err(|_| "Could not track local services.")? = children; if let Err(error) = wait_for_services(&log_directory) { - eprintln!("{error}"); + report_startup_error(&log_directory, error.as_ref()); services_for_setup.stop(); return Err(error); } @@ -355,7 +367,7 @@ pub fn run() { let editor_url = Url::parse(&format!("http://127.0.0.1:{FRONTEND_PORT}/editor"))?; let allowed_origin = editor_url.origin().ascii_serialization(); let window_actions = app.handle().clone(); - #[cfg(target_os = "linux")] + #[cfg(any(target_os = "linux", target_os = "windows"))] let page_load_marker = std::env::var_os("DOCUFLEX_PAGE_LOAD_MARKER").map(PathBuf::from); let window_builder = WebviewWindowBuilder::new(app, "main", WebviewUrl::External(editor_url)) @@ -370,7 +382,7 @@ pub fn run() { .traffic_light_position(LogicalPosition::new(24.0, 24.0)); #[cfg(target_os = "windows")] let window_builder = window_builder.decorations(false).shadow(true); - #[cfg(target_os = "linux")] + #[cfg(any(target_os = "linux", target_os = "windows"))] let window_builder = window_builder.on_page_load(move |_window, payload| { if payload.event() == PageLoadEvent::Finished && payload.url().path() == "/editor" { if let Some(marker) = &page_load_marker { diff --git a/desktop/src-tauri/src/main.rs b/desktop/src-tauri/src/main.rs index 286c9d7..ad5178e 100644 --- a/desktop/src-tauri/src/main.rs +++ b/desktop/src-tauri/src/main.rs @@ -1,3 +1,5 @@ +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + fn main() { docuflex_desktop_lib::run(); } From e45dff99d17c3d8a84660054bc8c9572e0d42f99 Mon Sep 17 00:00:00 2001 From: max06bayer <101212752+max06bayer@users.noreply.github.com> Date: Sat, 18 Jul 2026 20:46:06 +0200 Subject: [PATCH 15/32] Retry verified Windows runtime downloads --- desktop/scripts/prepare-windows-runtimes.ps1 | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/desktop/scripts/prepare-windows-runtimes.ps1 b/desktop/scripts/prepare-windows-runtimes.ps1 index 4c7f53a..a1a9b24 100644 --- a/desktop/scripts/prepare-windows-runtimes.ps1 +++ b/desktop/scripts/prepare-windows-runtimes.ps1 @@ -12,7 +12,8 @@ if (-not $RuntimeRoot.EndsWith($expectedSuffix, [StringComparison]::OrdinalIgnor function Get-VerifiedArchive { param([string]$Url, [string]$Path, [string]$Sha256) - Invoke-WebRequest -Uri $Url -OutFile $Path -UseBasicParsing + & curl.exe --location --fail --retry 5 --retry-all-errors --connect-timeout 30 --output $Path $Url + if ($LASTEXITCODE -ne 0) { throw "Download failed after retries: $Url" } $actual = (Get-FileHash -Path $Path -Algorithm SHA256).Hash.ToLowerInvariant() if ($actual -ne $Sha256) { throw "Checksum failed for $Url" } } @@ -62,9 +63,7 @@ $trainedData = @{ } foreach ($language in $trainedData.Keys) { $destination = Join-Path $ocrRuntime "share/tessdata/$language.traineddata" - Invoke-WebRequest -Uri "https://raw.githubusercontent.com/tesseract-ocr/tessdata_fast/4.1.0/$language.traineddata" -OutFile $destination -UseBasicParsing - $actual = (Get-FileHash -Path $destination -Algorithm SHA256).Hash.ToLowerInvariant() - if ($actual -ne $trainedData[$language]) { throw "Checksum failed for $language.traineddata" } + Get-VerifiedArchive -Url "https://raw.githubusercontent.com/tesseract-ocr/tessdata_fast/4.1.0/$language.traineddata" -Path $destination -Sha256 $trainedData[$language] } Remove-Item -LiteralPath $popplerArchive, $popplerExtract -Recurse -Force @' From da2a44a895e5b1169b885a632eb5d1d23e52261b Mon Sep 17 00:00:00 2001 From: max06bayer <101212752+max06bayer@users.noreply.github.com> Date: Sat, 18 Jul 2026 21:50:34 +0200 Subject: [PATCH 16/32] Test packaged Windows document tools --- .github/workflows/desktop-native-builds.yml | 30 ++++++++--- desktop/runtime/chrome.js | 6 +-- desktop/scripts/smoke-live-document-tools.mjs | 51 +++++++++++++++++++ 3 files changed, 77 insertions(+), 10 deletions(-) create mode 100644 desktop/scripts/smoke-live-document-tools.mjs diff --git a/.github/workflows/desktop-native-builds.yml b/.github/workflows/desktop-native-builds.yml index 869117b..6d524fb 100644 --- a/.github/workflows/desktop-native-builds.yml +++ b/.github/workflows/desktop-native-builds.yml @@ -63,17 +63,31 @@ jobs: shell: pwsh working-directory: desktop run: | - $runtimeRoot = Resolve-Path 'src-tauri/resources' - $releaseRoot = Resolve-Path 'src-tauri/target/release' - $releaseResources = Join-Path $releaseRoot 'resources' - if (Test-Path $releaseResources) { - Remove-Item -LiteralPath $releaseResources -Recurse -Force + $installer = Resolve-Path 'src-tauri/target/release/bundle/nsis/*.exe' + $install = Start-Process -FilePath $installer -ArgumentList '/S' -PassThru -Wait + if ($install.ExitCode -ne 0) { + throw "NSIS installation failed with exit code $($install.ExitCode)." + } + $entry = Get-ItemProperty 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*' | + Where-Object { $_.DisplayName -eq 'Docuflex' } | + Select-Object -First 1 + $candidates = @() + if ($entry.DisplayIcon) { + $candidates += ($entry.DisplayIcon -replace ',\d+$', '').Trim('"') + } + if ($entry.InstallLocation -and (Test-Path $entry.InstallLocation)) { + $candidates += Get-ChildItem $entry.InstallLocation -Filter '*.exe' -File | + Where-Object { $_.Name -notmatch 'uninstall' } | + Select-Object -ExpandProperty FullName + } + $appPath = $candidates | Where-Object { Test-Path $_ } | Select-Object -First 1 + if (-not $appPath) { + throw 'Could not locate the Docuflex executable after NSIS installation.' } - Copy-Item -LiteralPath $runtimeRoot -Destination $releaseResources -Recurse $marker = Join-Path $env:RUNNER_TEMP 'docuflex-windows-page-loaded' Remove-Item -LiteralPath $marker -Force -ErrorAction SilentlyContinue $env:DOCUFLEX_PAGE_LOAD_MARKER = $marker - $process = Start-Process -FilePath (Join-Path $releaseRoot 'docuflex-desktop.exe') -PassThru + $process = Start-Process -FilePath $appPath -PassThru try { foreach ($attempt in 1..60) { $process.Refresh() @@ -88,6 +102,8 @@ jobs: } catch {} if ($backendReady -and $frontendReady -and (Test-Path $marker) -and ((Get-Content $marker -Raw).Trim() -eq 'editor-loaded')) { Write-Host 'Packaged Windows services and WebView page-load test passed.' + node scripts/smoke-live-document-tools.mjs + if ($LASTEXITCODE -ne 0) { throw 'Packaged document-tool API tests failed.' } exit 0 } Start-Sleep -Seconds 1 diff --git a/desktop/runtime/chrome.js b/desktop/runtime/chrome.js index ca46814..3ed112f 100644 --- a/desktop/runtime/chrome.js +++ b/desktop/runtime/chrome.js @@ -270,7 +270,7 @@ } html[data-docuflex-desktop="windows"] .utilities { - padding-right: 138px !important; + padding-right: 168px !important; } html[data-docuflex-desktop="windows"] .topbar button, @@ -286,7 +286,7 @@ html[data-docuflex-desktop="windows"] .docuflex-windows-controls { display: flex; - height: 56px; + height: 32px; position: fixed; right: 0; top: 0; @@ -299,7 +299,7 @@ border: 0; color: #616161; display: flex; - height: 56px; + height: 32px; justify-content: center; padding: 0; position: relative; diff --git a/desktop/scripts/smoke-live-document-tools.mjs b/desktop/scripts/smoke-live-document-tools.mjs new file mode 100644 index 0000000..37487a5 --- /dev/null +++ b/desktop/scripts/smoke-live-document-tools.mjs @@ -0,0 +1,51 @@ +const frontend = process.env.DOCUFLEX_FRONTEND_URL || 'http://127.0.0.1:43127'; + +function minimalPdf(text) { + const objects = [ + '<< /Type /Catalog /Pages 2 0 R >>', + '<< /Type /Pages /Kids [3 0 R] /Count 1 >>', + '<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Resources << /Font << /F1 5 0 R >> >> /Contents 4 0 R >>', + `<< /Length ${text.length + 31} >>\nstream\nBT /F1 24 Tf 72 720 Td (${text}) Tj ET\nendstream`, + '<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>' + ]; + let pdf = '%PDF-1.4\n'; + const offsets = [0]; + objects.forEach((object, index) => { + offsets.push(Buffer.byteLength(pdf)); + pdf += `${index + 1} 0 obj\n${object}\nendobj\n`; + }); + const xref = Buffer.byteLength(pdf); + pdf += `xref\n0 ${objects.length + 1}\n0000000000 65535 f \n`; + pdf += offsets.slice(1).map((offset) => `${String(offset).padStart(10, '0')} 00000 n \n`).join(''); + pdf += `trailer\n<< /Size ${objects.length + 1} /Root 1 0 R >>\nstartxref\n${xref}\n%%EOF\n`; + return Buffer.from(pdf); +} + +async function expectSuccessfulResponse(label, response) { + if (response.ok) return response; + const detail = await response.text(); + throw new Error(`${label} failed with HTTP ${response.status}: ${detail}`); +} + +const pdf = minimalPdf('Docuflex Live Tools'); +const conversion = await expectSuccessfulResponse('Edit Text conversion', await fetch(`${frontend}/api/pdf/convert`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ pdfBase64: pdf.toString('base64') }) +})); +const conversionResult = await conversion.json(); +if (!conversionResult.htmlBase64 || !Buffer.from(conversionResult.htmlBase64, 'base64').includes('Docuflex')) { + throw new Error('Edit Text conversion did not return the expected document text.'); +} + +const ocr = await expectSuccessfulResponse('OCR', await fetch(`${frontend}/api/pdf/ocr`, { + method: 'POST', + headers: { 'Content-Type': 'application/pdf', 'x-ocr-languages': 'eng' }, + body: pdf +})); +const ocrBytes = Buffer.from(await ocr.arrayBuffer()); +if (!ocrBytes.subarray(0, 5).equals(Buffer.from('%PDF-'))) { + throw new Error('OCR did not return a PDF document.'); +} + +process.stdout.write('Packaged Edit Text and OCR API tests passed.\n'); From 3831b7f944b060675039deb5856dec0ecab54a0a Mon Sep 17 00:00:00 2001 From: max06bayer <101212752+max06bayer@users.noreply.github.com> Date: Sat, 18 Jul 2026 22:22:08 +0200 Subject: [PATCH 17/32] Bundle Windows document tool data --- desktop/runtime/pdf2htmlEX-windows-wrapper.rs | 16 ++++++- desktop/scripts/prepare-windows-runtimes.ps1 | 1 + desktop/scripts/smoke-live-document-tools.mjs | 43 ++++++++++++------- desktop/scripts/smoke-runtimes.mjs | 7 +++ 4 files changed, 50 insertions(+), 17 deletions(-) diff --git a/desktop/runtime/pdf2htmlEX-windows-wrapper.rs b/desktop/runtime/pdf2htmlEX-windows-wrapper.rs index 7e7e1c0..6c2632f 100644 --- a/desktop/runtime/pdf2htmlEX-windows-wrapper.rs +++ b/desktop/runtime/pdf2htmlEX-windows-wrapper.rs @@ -10,6 +10,7 @@ fn main() { .unwrap_or_else(|| PathBuf::from("pdf2htmlEX-native.exe")); let mut normalized = Vec::new(); + let mut data_directory = None; let mut arguments = env::args_os().skip(1); while let Some(argument) = arguments.next() { let option = argument.to_string_lossy(); @@ -17,10 +18,23 @@ fn main() { let _ = arguments.next(); continue; } + if option == "--data-dir" { + normalized.push(argument); + if let Some(value) = arguments.next() { + data_directory = Some(PathBuf::from(&value)); + normalized.push(value); + } + continue; + } normalized.push(argument); } - match Command::new(native).args(normalized).status() { + let mut command = Command::new(native); + command.args(normalized); + if let Some(directory) = data_directory { + command.current_dir(directory); + } + match command.status() { Ok(status) => exit(status.code().unwrap_or(1)), Err(error) => { eprintln!("Could not start bundled pdf2htmlEX: {error}"); diff --git a/desktop/scripts/prepare-windows-runtimes.ps1 b/desktop/scripts/prepare-windows-runtimes.ps1 index a1a9b24..9727ba6 100644 --- a/desktop/scripts/prepare-windows-runtimes.ps1 +++ b/desktop/scripts/prepare-windows-runtimes.ps1 @@ -53,6 +53,7 @@ $popplerBin = Join-Path $popplerExtract 'poppler-26.02.0/Library/bin' $ocrRuntime = Join-Path $RuntimeRoot 'ocr' New-Item -ItemType Directory -Force -Path (Join-Path $ocrRuntime 'bin'), (Join-Path $ocrRuntime 'poppler/bin'), (Join-Path $ocrRuntime 'share/tessdata') | Out-Null Copy-Item (Join-Path $tesseractRoot '*') (Join-Path $ocrRuntime 'bin') -Recurse -Force +Copy-Item (Join-Path $tesseractRoot 'tessdata/*') (Join-Path $ocrRuntime 'share/tessdata') -Recurse -Force Copy-Item (Join-Path $popplerBin '*') (Join-Path $ocrRuntime 'poppler/bin') -Recurse -Force Copy-Item (Join-Path $popplerExtract 'poppler-26.02.0/Library/share') (Join-Path $ocrRuntime 'share/poppler') -Recurse -Force diff --git a/desktop/scripts/smoke-live-document-tools.mjs b/desktop/scripts/smoke-live-document-tools.mjs index 37487a5..2df49b9 100644 --- a/desktop/scripts/smoke-live-document-tools.mjs +++ b/desktop/scripts/smoke-live-document-tools.mjs @@ -28,24 +28,35 @@ async function expectSuccessfulResponse(label, response) { } const pdf = minimalPdf('Docuflex Live Tools'); -const conversion = await expectSuccessfulResponse('Edit Text conversion', await fetch(`${frontend}/api/pdf/convert`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ pdfBase64: pdf.toString('base64') }) -})); -const conversionResult = await conversion.json(); -if (!conversionResult.htmlBase64 || !Buffer.from(conversionResult.htmlBase64, 'base64').includes('Docuflex')) { - throw new Error('Edit Text conversion did not return the expected document text.'); +const failures = []; +try { + const conversion = await expectSuccessfulResponse('Edit Text conversion', await fetch(`${frontend}/api/pdf/convert`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ pdfBase64: pdf.toString('base64') }) + })); + const conversionResult = await conversion.json(); + if (!conversionResult.htmlBase64 || !Buffer.from(conversionResult.htmlBase64, 'base64').includes('Docuflex')) { + throw new Error('Edit Text conversion did not return the expected document text.'); + } +} catch (error) { + failures.push(error); } -const ocr = await expectSuccessfulResponse('OCR', await fetch(`${frontend}/api/pdf/ocr`, { - method: 'POST', - headers: { 'Content-Type': 'application/pdf', 'x-ocr-languages': 'eng' }, - body: pdf -})); -const ocrBytes = Buffer.from(await ocr.arrayBuffer()); -if (!ocrBytes.subarray(0, 5).equals(Buffer.from('%PDF-'))) { - throw new Error('OCR did not return a PDF document.'); +try { + const ocr = await expectSuccessfulResponse('OCR', await fetch(`${frontend}/api/pdf/ocr`, { + method: 'POST', + headers: { 'Content-Type': 'application/pdf', 'x-ocr-languages': 'eng' }, + body: pdf + })); + const ocrBytes = Buffer.from(await ocr.arrayBuffer()); + if (!ocrBytes.subarray(0, 5).equals(Buffer.from('%PDF-'))) { + throw new Error('OCR did not return a PDF document.'); + } +} catch (error) { + failures.push(error); } +if (failures.length) throw new AggregateError(failures, 'Packaged document tools failed.'); + process.stdout.write('Packaged Edit Text and OCR API tests passed.\n'); diff --git a/desktop/scripts/smoke-runtimes.mjs b/desktop/scripts/smoke-runtimes.mjs index 1a9db92..cf8e7d7 100644 --- a/desktop/scripts/smoke-runtimes.mjs +++ b/desktop/scripts/smoke-runtimes.mjs @@ -76,6 +76,13 @@ try { timeout: 60_000 }); if (!/docuflex/i.test(ocrText)) throw new Error(`Bundled OCR smoke test failed: ${ocrText.trim()}`); + const ocrPdfBase = join(temporary, 'ocr-searchable'); + run(ocrExecutable('tesseract'), [image, ocrPdfBase, '--dpi', '300', '-l', 'eng', 'pdf'], { + env: { TESSDATA_PREFIX: join(resources, 'runtime', 'ocr', 'share', 'tessdata') }, + timeout: 60_000 + }); + const ocrPdf = await readFile(`${ocrPdfBase}.pdf`); + if (!ocrPdf.subarray(0, 5).equals(Buffer.from('%PDF-'))) throw new Error('Bundled OCR PDF smoke test failed.'); const pdf = join(temporary, 'document.pdf'); const html = join(temporary, 'document.html'); From 28187b6de4698b3cb1dddbff2ff7a10d2661e057 Mon Sep 17 00:00:00 2001 From: max06bayer <101212752+max06bayer@users.noreply.github.com> Date: Sat, 18 Jul 2026 22:48:26 +0200 Subject: [PATCH 18/32] Normalize packaged native tool paths --- desktop/scripts/prepare-linux-runtimes.sh | 9 +++++++++ desktop/src-tauri/src/lib.rs | 22 +++++++++++++++++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/desktop/scripts/prepare-linux-runtimes.sh b/desktop/scripts/prepare-linux-runtimes.sh index 9cde536..0e5ff9b 100644 --- a/desktop/scripts/prepare-linux-runtimes.sh +++ b/desktop/scripts/prepare-linux-runtimes.sh @@ -66,6 +66,15 @@ for data in eng deu osd; do test -n "$source_data" cp "$source_data" "$RUNTIME_ROOT/ocr/share/tessdata/$data.traineddata" done + +# Tesseract's searchable-PDF renderer loads these relative to TESSDATA_PREFIX. +# The traineddata files alone are enough for plain text OCR, but not PDF output. +for relative_data in configs/pdf pdf.ttf; do + source_data=$(find /usr/share -path "*/tessdata/$relative_data" -print -quit) + test -n "$source_data" + mkdir -p "$RUNTIME_ROOT/ocr/share/tessdata/$(dirname "$relative_data")" + cp "$source_data" "$RUNTIME_ROOT/ocr/share/tessdata/$relative_data" +done mkdir -p "$RUNTIME_ROOT/ocr/licenses" for package in poppler-utils tesseract-ocr tesseract-ocr-eng tesseract-ocr-deu tesseract-ocr-osd; do if [ -f "/usr/share/doc/$package/copyright" ]; then diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index a606881..70c3688 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -19,6 +19,26 @@ use url::Url; const FRONTEND_PORT: u16 = 43_127; const BACKEND_PORT: u16 = 43_128; +#[cfg(target_os = "windows")] +fn native_tool_path(path: PathBuf) -> PathBuf { + // Tauri may return verbatim (`\\?\`) paths for installed resources. Older + // native tools such as pdf2htmlEX 0.14 and Tesseract do not understand that + // prefix even though Node and Rust do. + let value = path.to_string_lossy(); + if let Some(rest) = value.strip_prefix(r"\\?\UNC\") { + return PathBuf::from(format!(r"\\{rest}")); + } + if let Some(rest) = value.strip_prefix(r"\\?\") { + return PathBuf::from(rest); + } + path +} + +#[cfg(not(target_os = "windows"))] +fn native_tool_path(path: PathBuf) -> PathBuf { + path +} + fn configure_platform_webview() { #[cfg(target_os = "linux")] { @@ -344,7 +364,7 @@ pub fn run() { }, )) .setup(move |app| { - let resource_root = app.path().resource_dir()?.join("resources"); + let resource_root = native_tool_path(app.path().resource_dir()?.join("resources")); let log_directory = app.path().app_log_dir()?; let children = match spawn_services(&resource_root, &log_directory) { Ok(children) => children, From 910120508dbe6aea62c0a4f905d33106d9c52e0a Mon Sep 17 00:00:00 2001 From: max06bayer <101212752+max06bayer@users.noreply.github.com> Date: Sun, 19 Jul 2026 12:35:30 +0200 Subject: [PATCH 19/32] Polish native PDF desktop integration --- .github/workflows/desktop-native-builds.yml | 6 ++ desktop/runtime/chrome.js | 100 +++++++++++++++---- desktop/scripts/package-arch-linux.sh | 4 +- desktop/src-tauri/Cargo.lock | 1 + desktop/src-tauri/Cargo.toml | 1 + desktop/src-tauri/src/lib.rs | 105 +++++++++++++++++++- desktop/src-tauri/tauri.conf.json | 10 ++ 7 files changed, 204 insertions(+), 23 deletions(-) diff --git a/.github/workflows/desktop-native-builds.yml b/.github/workflows/desktop-native-builds.yml index 6d524fb..bb05239 100644 --- a/.github/workflows/desktop-native-builds.yml +++ b/.github/workflows/desktop-native-builds.yml @@ -199,6 +199,7 @@ jobs: && grep --quiet '^compositing=1$' /tmp/docuflex-launcher-env 2>/dev/null \ && grep --quiet '^editor-loaded$' /tmp/docuflex-page-loaded 2>/dev/null; then echo "Packaged AppImage service and WebKit page-load test passed." + node scripts/smoke-live-document-tools.mjs exit 0 fi sleep 1 @@ -273,6 +274,11 @@ jobs: && curl --fail --silent http://127.0.0.1:43127/editor >/dev/null \ && grep --quiet '^editor-loaded$' /tmp/docuflex-page-loaded 2>/dev/null; then echo 'Native Arch services and WebKit page-load test passed.' + if ! node desktop/scripts/smoke-live-document-tools.mjs; then + find "$HOME/.local/share/com.docuflex.editor/logs" -type f -name '*.log' \ + -exec sh -c 'echo "[$1]"; cat "$1"' _ {} \; 2>/dev/null || true + exit 1 + fi exit 0 fi sleep 1 diff --git a/desktop/runtime/chrome.js b/desktop/runtime/chrome.js index 3ed112f..d39fcce 100644 --- a/desktop/runtime/chrome.js +++ b/desktop/runtime/chrome.js @@ -265,26 +265,36 @@ html[data-docuflex-desktop="windows"] .topbar, html[data-docuflex-desktop="windows"] .brand-area, html[data-docuflex-desktop="windows"] .tab-strip, - html[data-docuflex-desktop="windows"] .utilities { + html[data-docuflex-desktop="windows"] .utilities, + html[data-docuflex-desktop="linux"] .topbar, + html[data-docuflex-desktop="linux"] .brand-area, + html[data-docuflex-desktop="linux"] .tab-strip, + html[data-docuflex-desktop="linux"] .utilities { -webkit-app-region: drag; } - html[data-docuflex-desktop="windows"] .utilities { + html[data-docuflex-desktop="windows"] .utilities, + html[data-docuflex-desktop="linux"] .utilities { padding-right: 168px !important; } html[data-docuflex-desktop="windows"] .topbar button, html[data-docuflex-desktop="windows"] .topbar a, html[data-docuflex-desktop="windows"] .topbar input, - html[data-docuflex-desktop="windows"] .docuflex-windows-controls { + html[data-docuflex-desktop="linux"] .topbar button, + html[data-docuflex-desktop="linux"] .topbar a, + html[data-docuflex-desktop="linux"] .topbar input, + html[data-docuflex-desktop="windows"] .docuflex-window-controls, + html[data-docuflex-desktop="linux"] .docuflex-window-controls { -webkit-app-region: no-drag; } - .docuflex-windows-controls { + .docuflex-window-controls { display: none; } - html[data-docuflex-desktop="windows"] .docuflex-windows-controls { + html[data-docuflex-desktop="windows"] .docuflex-window-controls, + html[data-docuflex-desktop="linux"] .docuflex-window-controls { display: flex; height: 32px; position: fixed; @@ -293,7 +303,7 @@ z-index: 10002; } - .docuflex-windows-control { + .docuflex-window-control { align-items: center; background: transparent; border: 0; @@ -306,44 +316,44 @@ width: 46px; } - .docuflex-windows-control:hover { + .docuflex-window-control:hover { background: rgba(0, 0, 0, 0.07); color: #171717; } - .docuflex-windows-control.close:hover { + .docuflex-window-control.close:hover { background: #c42b1c; color: #fff; } - .docuflex-windows-control::before, - .docuflex-windows-control::after { + .docuflex-window-control::before, + .docuflex-window-control::after { box-sizing: border-box; content: ""; position: absolute; } - .docuflex-windows-control.minimize::before { + .docuflex-window-control.minimize::before { border-top: 1px solid currentColor; height: 1px; width: 10px; } - .docuflex-windows-control.maximize::before { + .docuflex-window-control.maximize::before { border: 1px solid currentColor; height: 10px; width: 10px; } - .docuflex-windows-control.close::before, - .docuflex-windows-control.close::after { + .docuflex-window-control.close::before, + .docuflex-window-control.close::after { background: currentColor; height: 1px; transform: rotate(45deg); width: 12px; } - .docuflex-windows-control.close::after { + .docuflex-window-control.close::after { transform: rotate(-45deg); } @@ -402,7 +412,6 @@ } const markDragRegion = () => { - if (desktopPlatform === 'linux') return; document .querySelectorAll('.topbar, .brand-area, .tab-strip, .utilities') .forEach((element) => element.setAttribute('data-tauri-drag-region', 'deep')); @@ -414,9 +423,9 @@ subtree: true, }); - if (desktopPlatform === 'windows' && !document.querySelector('.docuflex-windows-controls')) { + if (desktopPlatform !== 'macos' && !document.querySelector('.docuflex-window-controls')) { const controls = document.createElement('div'); - controls.className = 'docuflex-windows-controls'; + controls.className = 'docuflex-window-controls'; controls.setAttribute('aria-label', 'Window controls'); for (const [action, label] of [ ['minimize', 'Minimize'], @@ -424,7 +433,7 @@ ['close', 'Close'], ]) { const button = document.createElement('button'); - button.className = `docuflex-windows-control ${action}`; + button.className = `docuflex-window-control ${action}`; button.type = 'button'; button.setAttribute('aria-label', label); button.addEventListener('click', () => { @@ -442,6 +451,59 @@ installDesktopChrome(); } + if (desktopPlatform !== 'macos') { + const forwardedZoomEvents = new WeakSet(); + window.addEventListener('wheel', (event) => { + if (forwardedZoomEvents.has(event) || (!event.ctrlKey && !event.metaKey)) return; + event.preventDefault(); + event.stopImmediatePropagation(); + const slowed = new WheelEvent('wheel', { + bubbles: true, + cancelable: true, + composed: true, + clientX: event.clientX, + clientY: event.clientY, + ctrlKey: event.ctrlKey, + metaKey: event.metaKey, + deltaMode: event.deltaMode, + deltaX: event.deltaX * 0.32, + deltaY: event.deltaY * 0.32, + deltaZ: event.deltaZ * 0.32, + }); + forwardedZoomEvents.add(slowed); + event.target?.dispatchEvent(slowed); + }, { capture: true, passive: false }); + } + + const openPendingNativePdf = async () => { + const invoke = globalThis.__TAURI__?.core?.invoke; + if (typeof invoke !== 'function') return; + const pending = await invoke('take_pending_pdf').catch((error) => { + console.error('Could not read the PDF opened by the operating system:', error); + return null; + }); + if (!pending?.base64 || !pending?.name) return; + const binary = atob(pending.base64); + const bytes = new Uint8Array(binary.length); + for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index); + const input = document.querySelector('input.file-input[type="file"]'); + if (!(input instanceof HTMLInputElement)) { + console.error('Could not find the desktop PDF input.'); + return; + } + const transfer = new DataTransfer(); + transfer.items.add(new File([bytes], pending.name, { type: 'application/pdf', lastModified: Date.now() })); + input.files = transfer.files; + input.dispatchEvent(new Event('change', { bubbles: true })); + }; + + window.addEventListener('docuflex-native-open-pdf', () => void openPendingNativePdf()); + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', () => void openPendingNativePdf(), { once: true }); + } else { + void openPendingNativePdf(); + } + const exportButtonSelector = '.utilities .utility-button[aria-label="Download"], .utilities .utility-button[aria-label="Exporting PDF"]'; const exportFormats = [ ['pdf', 'PDF'], diff --git a/desktop/scripts/package-arch-linux.sh b/desktop/scripts/package-arch-linux.sh index 0413eb1..268668c 100644 --- a/desktop/scripts/package-arch-linux.sh +++ b/desktop/scripts/package-arch-linux.sh @@ -34,9 +34,11 @@ install -Dm644 /dev/stdin "$PACKAGE_ROOT/usr/share/applications/docuflex.desktop [Desktop Entry] Categories=Office; Comment=Offline PDF editor -Exec=docuflex-desktop %U +Exec=docuflex-desktop %F Icon=docuflex +MimeType=application/pdf; Name=Docuflex +StartupWMClass=docuflex-desktop Terminal=false Type=Application EOF diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock index 4580b69..1cbab0f 100644 --- a/desktop/src-tauri/Cargo.lock +++ b/desktop/src-tauri/Cargo.lock @@ -781,6 +781,7 @@ dependencies = [ name = "docuflex-desktop" version = "0.0.1" dependencies = [ + "base64 0.22.1", "libc", "rfd", "tauri", diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml index 71da4ab..6d671d3 100644 --- a/desktop/src-tauri/Cargo.toml +++ b/desktop/src-tauri/Cargo.toml @@ -13,6 +13,7 @@ crate-type = ["staticlib", "cdylib", "rlib"] tauri-build = { version = "2.5.6", features = [] } [dependencies] +base64 = "0.22.1" libc = "0.2.186" rfd = "0.17.2" tauri = { version = "2.11.5", features = [] } diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index 70c3688..68abe2e 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -1,4 +1,6 @@ +use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine as _}; use std::{ + collections::HashMap, ffi::OsString, fs::{self, File}, io::{Read, Write}, @@ -18,6 +20,75 @@ use url::Url; const FRONTEND_PORT: u16 = 43_127; const BACKEND_PORT: u16 = 43_128; +const MAX_OPEN_PDF_BYTES: u64 = 230 * 1024 * 1024; + +type PendingPdf = Arc>>; + +fn pdf_path_from_arguments(arguments: I, cwd: &Path) -> Option +where + I: IntoIterator, + S: AsRef, +{ + arguments.into_iter().skip(1).find_map(|argument| { + let value = argument.as_ref(); + let candidate = if let Ok(url) = Url::parse(value) { + if url.scheme() != "file" { + return None; + } + url.to_file_path().ok()? + } else { + let path = PathBuf::from(value); + if path.is_absolute() { + path + } else { + cwd.join(path) + } + }; + let is_pdf = candidate + .extension() + .and_then(|extension| extension.to_str()) + .is_some_and(|extension| extension.eq_ignore_ascii_case("pdf")); + (is_pdf && candidate.is_file()).then_some(candidate) + }) +} + +fn notify_pending_pdf(app: &tauri::AppHandle) { + if let Some(window) = app.get_webview_window("main") { + let _ = window.eval("window.dispatchEvent(new Event('docuflex-native-open-pdf'))"); + } +} + +#[tauri::command] +fn take_pending_pdf( + pending: tauri::State<'_, PendingPdf>, +) -> Result>, String> { + let path = pending + .lock() + .map_err(|_| "Could not access the pending PDF.".to_string())? + .take(); + let Some(path) = path else { + return Ok(None); + }; + let metadata = + fs::metadata(&path).map_err(|error| format!("Could not inspect the PDF: {error}"))?; + if !metadata.is_file() || metadata.len() == 0 || metadata.len() > MAX_OPEN_PDF_BYTES { + return Err("The selected PDF is empty or too large.".to_string()); + } + let bytes = fs::read(&path).map_err(|error| format!("Could not read the PDF: {error}"))?; + if !bytes.starts_with(b"%PDF-") { + return Err("The selected file is not a valid PDF.".to_string()); + } + let mut result = HashMap::new(); + result.insert( + "name".to_string(), + path.file_name() + .and_then(|name| name.to_str()) + .unwrap_or("document.pdf") + .to_string(), + ); + result.insert("base64".to_string(), BASE64_STANDARD.encode(bytes)); + Ok(Some(result)) +} #[cfg(target_os = "windows")] fn native_tool_path(path: PathBuf) -> PathBuf { @@ -347,6 +418,12 @@ fn editor_initialization_script() -> &'static str { #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { configure_platform_webview(); + let initial_pdf = std::env::current_dir() + .ok() + .and_then(|cwd| pdf_path_from_arguments(std::env::args(), &cwd)); + let pending_pdf: PendingPdf = Arc::new(Mutex::new(initial_pdf)); + let pending_pdf_for_instance = Arc::clone(&pending_pdf); + let pending_pdf_for_events = Arc::clone(&pending_pdf); let services = Arc::new(Services { children: Mutex::new(Vec::new()), }); @@ -354,8 +431,16 @@ pub fn run() { let services_for_exit = Arc::clone(&services); let app = tauri::Builder::default() + .manage(Arc::clone(&pending_pdf)) + .invoke_handler(tauri::generate_handler![take_pending_pdf]) .plugin(tauri_plugin_single_instance::init( - |app, _arguments, _cwd| { + move |app, arguments, cwd| { + if let Some(path) = pdf_path_from_arguments(arguments, Path::new(&cwd)) { + if let Ok(mut pending) = pending_pdf_for_instance.lock() { + *pending = Some(path); + } + notify_pending_pdf(app); + } if let Some(window) = app.get_webview_window("main") { let _ = window.show(); let _ = window.unminimize(); @@ -400,7 +485,7 @@ pub fn run() { .title_bar_style(TitleBarStyle::Overlay) .hidden_title(true) .traffic_light_position(LogicalPosition::new(24.0, 24.0)); - #[cfg(target_os = "windows")] + #[cfg(any(target_os = "linux", target_os = "windows"))] let window_builder = window_builder.decorations(false).shadow(true); #[cfg(any(target_os = "linux", target_os = "windows"))] let window_builder = window_builder.on_page_load(move |_window, payload| { @@ -459,7 +544,21 @@ pub fn run() { .build(tauri::generate_context!()) .expect("could not build Docuflex"); - app.run(move |_app_handle, event| { + app.run(move |app_handle, event| { + if let tauri::RunEvent::Opened { urls } = &event { + if let Some(path) = urls.iter().find_map(|url| { + let path = url.to_file_path().ok()?; + path.extension() + .and_then(|extension| extension.to_str()) + .is_some_and(|extension| extension.eq_ignore_ascii_case("pdf")) + .then_some(path) + }) { + if let Ok(mut pending) = pending_pdf_for_events.lock() { + *pending = Some(path); + } + notify_pending_pdf(app_handle); + } + } if matches!( event, tauri::RunEvent::Exit | tauri::RunEvent::ExitRequested { .. } diff --git a/desktop/src-tauri/tauri.conf.json b/desktop/src-tauri/tauri.conf.json index 481fe6b..bb5d313 100644 --- a/desktop/src-tauri/tauri.conf.json +++ b/desktop/src-tauri/tauri.conf.json @@ -7,6 +7,7 @@ "frontendDist": "../dist" }, "app": { + "withGlobalTauri": true, "windows": [], "security": { "csp": null @@ -15,6 +16,15 @@ "bundle": { "active": true, "targets": "all", + "fileAssociations": [ + { + "ext": ["pdf"], + "mimeType": "application/pdf", + "name": "PDF document", + "role": "Editor", + "rank": "Default" + } + ], "icon": ["icons/32x32.png", "icons/128x128.png", "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico"], "resources": ["resources/**/*"], "macOS": { From 2958dbb294eb50f485df6785a59875bd039c01d4 Mon Sep 17 00:00:00 2001 From: max06bayer <101212752+max06bayer@users.noreply.github.com> Date: Sun, 19 Jul 2026 12:39:46 +0200 Subject: [PATCH 20/32] Gate macOS file-open events --- desktop/src-tauri/src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index 68abe2e..ae97a9d 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -423,6 +423,7 @@ pub fn run() { .and_then(|cwd| pdf_path_from_arguments(std::env::args(), &cwd)); let pending_pdf: PendingPdf = Arc::new(Mutex::new(initial_pdf)); let pending_pdf_for_instance = Arc::clone(&pending_pdf); + #[cfg(target_os = "macos")] let pending_pdf_for_events = Arc::clone(&pending_pdf); let services = Arc::new(Services { children: Mutex::new(Vec::new()), @@ -545,6 +546,7 @@ pub fn run() { .expect("could not build Docuflex"); app.run(move |app_handle, event| { + #[cfg(target_os = "macos")] if let tauri::RunEvent::Opened { urls } = &event { if let Some(path) = urls.iter().find_map(|url| { let path = url.to_file_path().ok()?; From b02b59332e2c49737fc6424417fce01cf6a5552d Mon Sep 17 00:00:00 2001 From: max06bayer <101212752+max06bayer@users.noreply.github.com> Date: Sun, 19 Jul 2026 12:52:00 +0200 Subject: [PATCH 21/32] Capture packaged Linux tool logs --- .github/workflows/desktop-native-builds.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/desktop-native-builds.yml b/.github/workflows/desktop-native-builds.yml index bb05239..00ff72e 100644 --- a/.github/workflows/desktop-native-builds.yml +++ b/.github/workflows/desktop-native-builds.yml @@ -199,7 +199,11 @@ jobs: && grep --quiet '^compositing=1$' /tmp/docuflex-launcher-env 2>/dev/null \ && grep --quiet '^editor-loaded$' /tmp/docuflex-page-loaded 2>/dev/null; then echo "Packaged AppImage service and WebKit page-load test passed." - node scripts/smoke-live-document-tools.mjs + if ! node scripts/smoke-live-document-tools.mjs; then + find "$HOME/.local/share/com.docuflex.editor/logs" -type f -name '*.log' \ + -exec sh -c 'echo "[$1]"; cat "$1"' _ {} \; 2>/dev/null || true + exit 1 + fi exit 0 fi sleep 1 From 3d866f5f19287f5fcaa7c084b93f35fb75316641 Mon Sep 17 00:00:00 2001 From: max06bayer <101212752+max06bayer@users.noreply.github.com> Date: Sun, 19 Jul 2026 13:10:17 +0200 Subject: [PATCH 22/32] Run bundled Linux document tools directly --- desktop/src-tauri/src/lib.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index ae97a9d..edae7ce 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -302,6 +302,11 @@ fn spawn_services( format!("http://127.0.0.1:{BACKEND_PORT}"), ) .env("BODY_SIZE_LIMIT", (230_u64 * 1024 * 1024).to_string()) + // The desktop invokes only its checksum-pinned bundled tools. Linux's + // RLIMIT_NPROC counts every process owned by the logged-in user, so the + // server profile's limit can prevent pdf2htmlEX from forking on a + // normal KDE session and can hide OCR fallback ENOENT detection. + .env("DISABLE_DOCUMENT_PRLIMIT", "1") .env("ADDRESS_HEADER", "") .env("PROTOCOL_HEADER", "") .env("HOST_HEADER", "") From 42b8b0ba70d60cc869a5cb85af7230772e3ec96d Mon Sep 17 00:00:00 2001 From: max06bayer <101212752+max06bayer@users.noreply.github.com> Date: Sun, 19 Jul 2026 13:34:25 +0200 Subject: [PATCH 23/32] Use native Arch document tools --- .github/workflows/desktop-native-builds.yml | 2 +- desktop/runtime/pdf2htmlEX-linux-wrapper.sh | 14 ++++++++++++++ desktop/scripts/package-arch-linux.sh | 13 +++++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/.github/workflows/desktop-native-builds.yml b/.github/workflows/desktop-native-builds.yml index 00ff72e..5140143 100644 --- a/.github/workflows/desktop-native-builds.yml +++ b/.github/workflows/desktop-native-builds.yml @@ -232,7 +232,7 @@ jobs: pacman -Syu --noconfirm pacman -S --noconfirm --needed \ base-devel binutils curl dbus git gtk3 libayatana-appindicator \ - librsvg nodejs npm rust webkit2gtk-4.1 xorg-server-xvfb + librsvg nodejs npm poppler rust tesseract webkit2gtk-4.1 xorg-server-xvfb - uses: actions/checkout@v4 - uses: actions/download-artifact@v4 with: diff --git a/desktop/runtime/pdf2htmlEX-linux-wrapper.sh b/desktop/runtime/pdf2htmlEX-linux-wrapper.sh index 2d117b8..22e42d9 100644 --- a/desktop/runtime/pdf2htmlEX-linux-wrapper.sh +++ b/desktop/runtime/pdf2htmlEX-linux-wrapper.sh @@ -4,6 +4,20 @@ set -eu SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) APP_ROOT="$SCRIPT_DIR/../app" +# AppRun is itself dynamically linked. On rolling distributions the host may +# no longer provide compatibility sonames such as libxml2.so.2, so activate +# the extracted AppImage libraries before starting it. +for library_dir in \ + "$APP_ROOT/usr/lib/x86_64-linux-gnu" \ + "$APP_ROOT/usr/lib" \ + "$APP_ROOT/lib/x86_64-linux-gnu" \ + "$APP_ROOT/lib"; do + if [ -d "$library_dir" ]; then + LD_LIBRARY_PATH="$library_dir${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" + fi +done +export LD_LIBRARY_PATH + normalized_args=() while [ "$#" -gt 0 ]; do if [ "$1" = "--embed" ] && [ "$#" -ge 2 ] && [ "$2" = "1" ]; then diff --git a/desktop/scripts/package-arch-linux.sh b/desktop/scripts/package-arch-linux.sh index 268668c..ee10a3b 100644 --- a/desktop/scripts/package-arch-linux.sh +++ b/desktop/scripts/package-arch-linux.sh @@ -28,6 +28,17 @@ install -Dm755 "$TAURI_ROOT/target/release/docuflex-desktop" \ "$PACKAGE_ROOT/usr/bin/docuflex-desktop" mkdir -p "$PACKAGE_ROOT/usr/lib/$RESOURCE_NAME" cp -a "$RESOURCE_SOURCE/." "$PACKAGE_ROOT/usr/lib/$RESOURCE_NAME/" + +# The Debian payload carries Ubuntu-compatible OCR binaries for AppImage and +# Debian users. A native Arch package must use current Arch Poppler/Tesseract +# binaries instead of mixing Ubuntu ELF dependencies with rolling libraries. +for tool in pdftoppm pdfunite tesseract; do + install -Dm755 /dev/stdin \ + "$PACKAGE_ROOT/usr/lib/$RESOURCE_NAME/runtime/ocr/bin/$tool" < Date: Sun, 19 Jul 2026 13:40:34 +0200 Subject: [PATCH 24/32] Isolate Arch pdf2htmlEX compatibility library --- desktop/runtime/pdf2htmlEX-linux-wrapper.sh | 14 --------- desktop/scripts/package-arch-linux.sh | 32 +++++++++++++++++++++ 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/desktop/runtime/pdf2htmlEX-linux-wrapper.sh b/desktop/runtime/pdf2htmlEX-linux-wrapper.sh index 22e42d9..2d117b8 100644 --- a/desktop/runtime/pdf2htmlEX-linux-wrapper.sh +++ b/desktop/runtime/pdf2htmlEX-linux-wrapper.sh @@ -4,20 +4,6 @@ set -eu SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) APP_ROOT="$SCRIPT_DIR/../app" -# AppRun is itself dynamically linked. On rolling distributions the host may -# no longer provide compatibility sonames such as libxml2.so.2, so activate -# the extracted AppImage libraries before starting it. -for library_dir in \ - "$APP_ROOT/usr/lib/x86_64-linux-gnu" \ - "$APP_ROOT/usr/lib" \ - "$APP_ROOT/lib/x86_64-linux-gnu" \ - "$APP_ROOT/lib"; do - if [ -d "$library_dir" ]; then - LD_LIBRARY_PATH="$library_dir${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" - fi -done -export LD_LIBRARY_PATH - normalized_args=() while [ "$#" -gt 0 ]; do if [ "$1" = "--embed" ] && [ "$#" -ge 2 ] && [ "$2" = "1" ]; then diff --git a/desktop/scripts/package-arch-linux.sh b/desktop/scripts/package-arch-linux.sh index ee10a3b..b2b6c5f 100644 --- a/desktop/scripts/package-arch-linux.sh +++ b/desktop/scripts/package-arch-linux.sh @@ -39,6 +39,38 @@ for tool in pdftoppm pdfunite tesseract; do exec /usr/bin/$tool "\$@" EOF done + +# pdf2htmlEX remains the checksum-pinned extracted AppImage build, but its +# launcher needs only the payload's legacy libxml2 soname on rolling Arch. +PDF2HTML_RUNTIME="$PACKAGE_ROOT/usr/lib/$RESOURCE_NAME/runtime/pdf2htmlEX" +mkdir -p "$PDF2HTML_RUNTIME/compat" +LIBXML_SOURCE=$(find "$PDF2HTML_RUNTIME/app" -type f -name 'libxml2.so.2*' -print -quit) +if [ -z "$LIBXML_SOURCE" ]; then + echo 'Bundled pdf2htmlEX payload does not contain libxml2.so.2.' >&2 + exit 1 +fi +cp -L "$LIBXML_SOURCE" "$PDF2HTML_RUNTIME/compat/libxml2.so.2" +install -Dm755 /dev/stdin \ + "$PDF2HTML_RUNTIME/bin/pdf2htmlEX" <<'EOF' +#!/bin/bash +set -eu +SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +APP_ROOT="$SCRIPT_DIR/../app" +export APPDIR="$APP_ROOT" +export LD_LIBRARY_PATH="$SCRIPT_DIR/../compat${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" + +normalized_args=() +while [ "$#" -gt 0 ]; do + if [ "$1" = "--embed" ] && [ "$#" -ge 2 ] && [ "$2" = "1" ]; then + normalized_args+=(--embed-css 1 --embed-font 1 --embed-image 1 --embed-javascript 1 --embed-outline 1) + shift 2 + continue + fi + normalized_args+=("$1") + shift +done +exec "$APP_ROOT/AppRun" "${normalized_args[@]}" +EOF install -Dm644 "$TAURI_ROOT/icons/128x128.png" \ "$PACKAGE_ROOT/usr/share/icons/hicolor/128x128/apps/docuflex.png" install -Dm644 /dev/stdin "$PACKAGE_ROOT/usr/share/applications/docuflex.desktop" <<'EOF' From 7b7a8e3f7400f444b453c1c24df0f2da77f8175d Mon Sep 17 00:00:00 2001 From: max06bayer <101212752+max06bayer@users.noreply.github.com> Date: Sun, 19 Jul 2026 14:04:00 +0200 Subject: [PATCH 25/32] Target installed Arch runtime resources --- desktop/scripts/package-arch-linux.sh | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/desktop/scripts/package-arch-linux.sh b/desktop/scripts/package-arch-linux.sh index b2b6c5f..fcc8657 100644 --- a/desktop/scripts/package-arch-linux.sh +++ b/desktop/scripts/package-arch-linux.sh @@ -28,13 +28,20 @@ install -Dm755 "$TAURI_ROOT/target/release/docuflex-desktop" \ "$PACKAGE_ROOT/usr/bin/docuflex-desktop" mkdir -p "$PACKAGE_ROOT/usr/lib/$RESOURCE_NAME" cp -a "$RESOURCE_SOURCE/." "$PACKAGE_ROOT/usr/lib/$RESOURCE_NAME/" +INSTALLED_RESOURCE_ROOT="$PACKAGE_ROOT/usr/lib/$RESOURCE_NAME" +if [ -d "$INSTALLED_RESOURCE_ROOT/resources/runtime" ]; then + RUNTIME_ROOT="$INSTALLED_RESOURCE_ROOT/resources/runtime" +else + RUNTIME_ROOT="$INSTALLED_RESOURCE_ROOT/runtime" +fi +test -d "$RUNTIME_ROOT" # The Debian payload carries Ubuntu-compatible OCR binaries for AppImage and # Debian users. A native Arch package must use current Arch Poppler/Tesseract # binaries instead of mixing Ubuntu ELF dependencies with rolling libraries. for tool in pdftoppm pdfunite tesseract; do install -Dm755 /dev/stdin \ - "$PACKAGE_ROOT/usr/lib/$RESOURCE_NAME/runtime/ocr/bin/$tool" < Date: Sun, 19 Jul 2026 14:28:29 +0200 Subject: [PATCH 26/32] Resolve Arch pdf2htmlEX compatibility closure --- desktop/scripts/package-arch-linux.sh | 30 +++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/desktop/scripts/package-arch-linux.sh b/desktop/scripts/package-arch-linux.sh index fcc8657..456a6b8 100644 --- a/desktop/scripts/package-arch-linux.sh +++ b/desktop/scripts/package-arch-linux.sh @@ -47,16 +47,33 @@ exec /usr/bin/$tool "\$@" EOF done -# pdf2htmlEX remains the checksum-pinned extracted AppImage build, but its -# launcher needs only the payload's legacy libxml2 soname on rolling Arch. +# pdf2htmlEX remains the checksum-pinned extracted AppImage build. Copy only +# sonames absent from rolling Arch into an isolated compatibility directory; +# exposing the payload's whole Ubuntu library tree would conflict with host +# GTK/GLib while resolving too little breaks on legacy ICU/libxml sonames. PDF2HTML_RUNTIME="$RUNTIME_ROOT/pdf2htmlEX" mkdir -p "$PDF2HTML_RUNTIME/compat" -LIBXML_SOURCE=$(find "$PDF2HTML_RUNTIME/app" -type f -name 'libxml2.so.2*' -print -quit) -if [ -z "$LIBXML_SOURCE" ]; then - echo 'Bundled pdf2htmlEX payload does not contain libxml2.so.2.' >&2 +for pass in 1 2 3 4 5 6 7 8; do + missing_libraries=$(LD_LIBRARY_PATH="$PDF2HTML_RUNTIME/compat" \ + ldd "$PDF2HTML_RUNTIME/app/AppRun" | awk '/=> not found/ { print $1 }') + [ -n "$missing_libraries" ] || break + while IFS= read -r library; do + [ -n "$library" ] || continue + library_source=$(find "$PDF2HTML_RUNTIME/app" -name "$library" -print -quit) + if [ -z "$library_source" ]; then + echo "Bundled pdf2htmlEX payload does not contain $library." >&2 + exit 1 + fi + cp -L "$library_source" "$PDF2HTML_RUNTIME/compat/$library" + done < not found'; then + echo 'Bundled pdf2htmlEX still has unresolved Arch compatibility libraries.' >&2 exit 1 fi -cp -L "$LIBXML_SOURCE" "$PDF2HTML_RUNTIME/compat/libxml2.so.2" install -Dm755 /dev/stdin \ "$PDF2HTML_RUNTIME/bin/pdf2htmlEX" <<'EOF' #!/bin/bash @@ -78,6 +95,7 @@ while [ "$#" -gt 0 ]; do done exec "$APP_ROOT/AppRun" "${normalized_args[@]}" EOF +"$PDF2HTML_RUNTIME/bin/pdf2htmlEX" --version >/dev/null install -Dm644 "$TAURI_ROOT/icons/128x128.png" \ "$PACKAGE_ROOT/usr/share/icons/hicolor/128x128/apps/docuflex.png" install -Dm644 /dev/stdin "$PACKAGE_ROOT/usr/share/applications/docuflex.desktop" <<'EOF' From 26adcdd34576cc32174c9eef1654d00c291ede56 Mon Sep 17 00:00:00 2001 From: max06bayer <101212752+max06bayer@users.noreply.github.com> Date: Sun, 19 Jul 2026 16:41:23 +0200 Subject: [PATCH 27/32] Fix Linux window drag and OCR rendering --- desktop/runtime/chrome.js | 31 +++++++++++++++++-- desktop/scripts/package-arch-linux.sh | 14 ++++++++- desktop/scripts/smoke-live-document-tools.mjs | 3 ++ 3 files changed, 44 insertions(+), 4 deletions(-) diff --git a/desktop/runtime/chrome.js b/desktop/runtime/chrome.js index d39fcce..7f98250 100644 --- a/desktop/runtime/chrome.js +++ b/desktop/runtime/chrome.js @@ -262,6 +262,12 @@ zoom: 1 !important; } + html[data-docuflex-desktop="linux"] .text-highlight-layer .text-highlight, + html[data-docuflex-desktop="linux"] [data-docuflex-annotation-type="highlight"] rect { + mix-blend-mode: multiply !important; + opacity: 0.42 !important; + } + html[data-docuflex-desktop="windows"] .topbar, html[data-docuflex-desktop="windows"] .brand-area, html[data-docuflex-desktop="windows"] .tab-strip, @@ -412,9 +418,14 @@ } const markDragRegion = () => { - document - .querySelectorAll('.topbar, .brand-area, .tab-strip, .utilities') - .forEach((element) => element.setAttribute('data-tauri-drag-region', 'deep')); + document.querySelectorAll('.topbar, .brand-area, .tab-strip, .utilities').forEach((element) => { + element.setAttribute('data-tauri-drag-region', ''); + element.querySelectorAll('*').forEach((child) => { + if (!child.closest('button, a, input, select, textarea, [role="button"]')) { + child.setAttribute('data-tauri-drag-region', ''); + } + }); + }); }; markDragRegion(); @@ -423,6 +434,20 @@ subtree: true, }); + if (desktopPlatform === 'linux' && !document.documentElement.dataset.docuflexLinuxDragInstalled) { + document.documentElement.dataset.docuflexLinuxDragInstalled = 'true'; + document.addEventListener('pointerdown', (event) => { + if (event.button !== 0 || !(event.target instanceof Element)) return; + if (!event.target.closest('.topbar')) return; + if (event.target.closest('button, a, input, select, textarea, [role="button"], .docuflex-window-controls')) return; + const appWindow = globalThis.__TAURI__?.window?.getCurrentWindow?.(); + if (typeof appWindow?.startDragging === 'function') { + event.preventDefault(); + void appWindow.startDragging(); + } + }, { capture: true }); + } + if (desktopPlatform !== 'macos' && !document.querySelector('.docuflex-window-controls')) { const controls = document.createElement('div'); controls.className = 'docuflex-window-controls'; diff --git a/desktop/scripts/package-arch-linux.sh b/desktop/scripts/package-arch-linux.sh index 456a6b8..f226a81 100644 --- a/desktop/scripts/package-arch-linux.sh +++ b/desktop/scripts/package-arch-linux.sh @@ -39,13 +39,25 @@ test -d "$RUNTIME_ROOT" # The Debian payload carries Ubuntu-compatible OCR binaries for AppImage and # Debian users. A native Arch package must use current Arch Poppler/Tesseract # binaries instead of mixing Ubuntu ELF dependencies with rolling libraries. -for tool in pdftoppm pdfunite tesseract; do +for tool in pdftoppm pdfunite; do install -Dm755 /dev/stdin \ "$RUNTIME_ROOT/ocr/bin/$tool" < Date: Sun, 19 Jul 2026 17:23:50 +0200 Subject: [PATCH 28/32] Use native Linux window drag and OCR wrapper --- desktop/runtime/chrome.js | 8 +++++--- desktop/runtime/native-tool-linux-wrapper.sh | 12 +++++++++++- desktop/src-tauri/src/lib.rs | 15 ++++++++++++++- 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/desktop/runtime/chrome.js b/desktop/runtime/chrome.js index 7f98250..27e3330 100644 --- a/desktop/runtime/chrome.js +++ b/desktop/runtime/chrome.js @@ -440,10 +440,12 @@ if (event.button !== 0 || !(event.target instanceof Element)) return; if (!event.target.closest('.topbar')) return; if (event.target.closest('button, a, input, select, textarea, [role="button"], .docuflex-window-controls')) return; - const appWindow = globalThis.__TAURI__?.window?.getCurrentWindow?.(); - if (typeof appWindow?.startDragging === 'function') { + const invoke = globalThis.__TAURI__?.core?.invoke; + if (typeof invoke === 'function') { event.preventDefault(); - void appWindow.startDragging(); + void invoke('start_window_drag').catch((error) => { + console.error('Could not start native window dragging:', error); + }); } }, { capture: true }); } diff --git a/desktop/runtime/native-tool-linux-wrapper.sh b/desktop/runtime/native-tool-linux-wrapper.sh index 0dbbd88..2ca7c75 100644 --- a/desktop/runtime/native-tool-linux-wrapper.sh +++ b/desktop/runtime/native-tool-linux-wrapper.sh @@ -1,7 +1,17 @@ -#!/bin/sh +#!/bin/bash set -eu SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) TOOL_NAME=${0##*/} export LD_LIBRARY_PATH="$SCRIPT_DIR/../lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" +if [ "$TOOL_NAME" = "tesseract" ]; then + arguments=() + for argument in "$@"; do + if [ "$argument" = "pdf" ]; then + arguments+=(-c textonly_pdf=0) + fi + arguments+=("$argument") + done + exec "$SCRIPT_DIR/$TOOL_NAME-native" "${arguments[@]}" +fi exec "$SCRIPT_DIR/$TOOL_NAME-native" "$@" diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index edae7ce..6d46fc3 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -90,6 +90,16 @@ fn take_pending_pdf( Ok(Some(result)) } +#[tauri::command] +fn start_window_drag(app: tauri::AppHandle) -> Result<(), String> { + let window = app + .get_webview_window("main") + .ok_or_else(|| "The Docuflex window is unavailable.".to_string())?; + window + .start_dragging() + .map_err(|error| format!("Could not drag the Docuflex window: {error}")) +} + #[cfg(target_os = "windows")] fn native_tool_path(path: PathBuf) -> PathBuf { // Tauri may return verbatim (`\\?\`) paths for installed resources. Older @@ -438,7 +448,10 @@ pub fn run() { let app = tauri::Builder::default() .manage(Arc::clone(&pending_pdf)) - .invoke_handler(tauri::generate_handler![take_pending_pdf]) + .invoke_handler(tauri::generate_handler![ + take_pending_pdf, + start_window_drag + ]) .plugin(tauri_plugin_single_instance::init( move |app, arguments, cwd| { if let Some(path) = pdf_path_from_arguments(arguments, Path::new(&cwd)) { From 86c642bdc89e8e71a6c93c7029d1020ceb779d7a Mon Sep 17 00:00:00 2001 From: max06bayer <101212752+max06bayer@users.noreply.github.com> Date: Sun, 19 Jul 2026 18:54:43 +0200 Subject: [PATCH 29/32] Fix Linux native drag event and OCR reload bytes --- desktop/runtime/chrome.js | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/desktop/runtime/chrome.js b/desktop/runtime/chrome.js index 27e3330..8b66355 100644 --- a/desktop/runtime/chrome.js +++ b/desktop/runtime/chrome.js @@ -5,6 +5,18 @@ ? 'linux' : 'macos'; const desktopBlobUrls = new Map(); + const NativeDesktopFile = globalThis.File; + const desktopConstructedFileBytes = new WeakMap(); + if (typeof NativeDesktopFile === 'function') { + globalThis.File = class DocuflexDesktopFile extends NativeDesktopFile { + constructor(parts, name, options) { + super(parts, name, options); + if (parts?.length === 1 && parts[0] instanceof ArrayBuffer) { + desktopConstructedFileBytes.set(this, parts[0].slice(0)); + } + } + }; + } const originalCreateObjectUrl = URL.createObjectURL.bind(URL); const originalRevokeObjectUrl = URL.revokeObjectURL.bind(URL); URL.createObjectURL = (object) => { @@ -74,7 +86,9 @@ configurable: true, enumerable: false, value() { - if (!(this instanceof File)) return originalArrayBuffer.call(this); + if (!(this instanceof NativeDesktopFile)) return originalArrayBuffer.call(this); + const constructedBytes = desktopConstructedFileBytes.get(this); + if (constructedBytes) return Promise.resolve(constructedBytes.slice(0)); let active = activeReads.get(this); if (!active) { const key = fileKey(this); @@ -284,6 +298,13 @@ padding-right: 168px !important; } + html[data-docuflex-desktop="linux"] .topbar, + html[data-docuflex-desktop="linux"] .brand-area, + html[data-docuflex-desktop="linux"] .tab-strip, + html[data-docuflex-desktop="linux"] .utilities { + -webkit-app-region: no-drag !important; + } + html[data-docuflex-desktop="windows"] .topbar button, html[data-docuflex-desktop="windows"] .topbar a, html[data-docuflex-desktop="windows"] .topbar input, @@ -418,6 +439,7 @@ } const markDragRegion = () => { + if (desktopPlatform === 'linux') return; document.querySelectorAll('.topbar, .brand-area, .tab-strip, .utilities').forEach((element) => { element.setAttribute('data-tauri-drag-region', ''); element.querySelectorAll('*').forEach((child) => { From 610141b747ec45e6fd7983e87feccea4431964bb Mon Sep 17 00:00:00 2001 From: max06bayer <101212752+max06bayer@users.noreply.github.com> Date: Sun, 19 Jul 2026 20:07:11 +0200 Subject: [PATCH 30/32] Fix authorized Linux drag regions and verify OCR pixels --- desktop/runtime/chrome.js | 44 +++++++------------ desktop/scripts/smoke-live-document-tools.mjs | 30 +++++++++++++ desktop/src-tauri/capabilities/main.json | 5 +-- 3 files changed, 47 insertions(+), 32 deletions(-) diff --git a/desktop/runtime/chrome.js b/desktop/runtime/chrome.js index 8b66355..1e33625 100644 --- a/desktop/runtime/chrome.js +++ b/desktop/runtime/chrome.js @@ -8,14 +8,24 @@ const NativeDesktopFile = globalThis.File; const desktopConstructedFileBytes = new WeakMap(); if (typeof NativeDesktopFile === 'function') { - globalThis.File = class DocuflexDesktopFile extends NativeDesktopFile { - constructor(parts, name, options) { - super(parts, name, options); - if (parts?.length === 1 && parts[0] instanceof ArrayBuffer) { - desktopConstructedFileBytes.set(this, parts[0].slice(0)); + const DocuflexDesktopFile = function (parts, name, options) { + const file = new NativeDesktopFile(parts, name, options); + if (parts?.length === 1) { + const part = parts[0]; + if (part instanceof ArrayBuffer) { + desktopConstructedFileBytes.set(file, part.slice(0)); + } else if (ArrayBuffer.isView(part)) { + desktopConstructedFileBytes.set( + file, + part.buffer.slice(part.byteOffset, part.byteOffset + part.byteLength), + ); } } + return file; }; + Object.setPrototypeOf(DocuflexDesktopFile, NativeDesktopFile); + DocuflexDesktopFile.prototype = NativeDesktopFile.prototype; + globalThis.File = DocuflexDesktopFile; } const originalCreateObjectUrl = URL.createObjectURL.bind(URL); const originalRevokeObjectUrl = URL.revokeObjectURL.bind(URL); @@ -298,13 +308,6 @@ padding-right: 168px !important; } - html[data-docuflex-desktop="linux"] .topbar, - html[data-docuflex-desktop="linux"] .brand-area, - html[data-docuflex-desktop="linux"] .tab-strip, - html[data-docuflex-desktop="linux"] .utilities { - -webkit-app-region: no-drag !important; - } - html[data-docuflex-desktop="windows"] .topbar button, html[data-docuflex-desktop="windows"] .topbar a, html[data-docuflex-desktop="windows"] .topbar input, @@ -439,7 +442,6 @@ } const markDragRegion = () => { - if (desktopPlatform === 'linux') return; document.querySelectorAll('.topbar, .brand-area, .tab-strip, .utilities').forEach((element) => { element.setAttribute('data-tauri-drag-region', ''); element.querySelectorAll('*').forEach((child) => { @@ -456,22 +458,6 @@ subtree: true, }); - if (desktopPlatform === 'linux' && !document.documentElement.dataset.docuflexLinuxDragInstalled) { - document.documentElement.dataset.docuflexLinuxDragInstalled = 'true'; - document.addEventListener('pointerdown', (event) => { - if (event.button !== 0 || !(event.target instanceof Element)) return; - if (!event.target.closest('.topbar')) return; - if (event.target.closest('button, a, input, select, textarea, [role="button"], .docuflex-window-controls')) return; - const invoke = globalThis.__TAURI__?.core?.invoke; - if (typeof invoke === 'function') { - event.preventDefault(); - void invoke('start_window_drag').catch((error) => { - console.error('Could not start native window dragging:', error); - }); - } - }, { capture: true }); - } - if (desktopPlatform !== 'macos' && !document.querySelector('.docuflex-window-controls')) { const controls = document.createElement('div'); controls.className = 'docuflex-window-controls'; diff --git a/desktop/scripts/smoke-live-document-tools.mjs b/desktop/scripts/smoke-live-document-tools.mjs index b147b0f..1ef6684 100644 --- a/desktop/scripts/smoke-live-document-tools.mjs +++ b/desktop/scripts/smoke-live-document-tools.mjs @@ -1,5 +1,34 @@ +import { execFileSync } from 'node:child_process'; +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + const frontend = process.env.DOCUFLEX_FRONTEND_URL || 'http://127.0.0.1:43127'; +function assertPdfHasVisiblePixels(pdfBytes) { + const directory = mkdtempSync(join(tmpdir(), 'docuflex-ocr-visual-')); + try { + const pdfPath = join(directory, 'ocr.pdf'); + const imagePrefix = join(directory, 'page'); + writeFileSync(pdfPath, pdfBytes); + execFileSync('pdftoppm', ['-f', '1', '-singlefile', '-r', '96', '-gray', pdfPath, imagePrefix], { + stdio: 'pipe' + }); + const pgm = readFileSync(`${imagePrefix}.pgm`); + const header = pgm.toString('ascii', 0, Math.min(pgm.length, 256)); + const match = header.match(/^P5\s+(?:#.*\s+)*(\d+)\s+(\d+)\s+(\d+)\s/); + if (!match) throw new Error('OCR visual check could not parse the rendered page.'); + const headerLength = match[0].length; + const pixels = pgm.subarray(headerLength); + const visiblePixels = pixels.reduce((count, value) => count + (value < 245 ? 1 : 0), 0); + if (visiblePixels < 100) { + throw new Error(`OCR rendered a blank page (${visiblePixels} non-white pixels).`); + } + } finally { + rmSync(directory, { recursive: true, force: true }); + } +} + function minimalPdf(text) { const objects = [ '<< /Type /Catalog /Pages 2 0 R >>', @@ -56,6 +85,7 @@ try { if (ocrBytes.length < 5_000 || ocrBytes.length < pdf.length * 3) { throw new Error(`OCR returned a suspiciously small, potentially blank PDF (${ocrBytes.length} bytes).`); } + assertPdfHasVisiblePixels(ocrBytes); } catch (error) { failures.push(error); } diff --git a/desktop/src-tauri/capabilities/main.json b/desktop/src-tauri/capabilities/main.json index f960699..44f00de 100644 --- a/desktop/src-tauri/capabilities/main.json +++ b/desktop/src-tauri/capabilities/main.json @@ -1,7 +1,7 @@ { "$schema": "../gen/schemas/desktop-schema.json", "identifier": "main-window", - "description": "Allows the packaged macOS editor window to use its custom native drag regions.", + "description": "Allows the packaged desktop editor window to use its custom native drag regions.", "remote": { "urls": ["http://127.0.0.1:43127/*"] }, @@ -9,6 +9,5 @@ "permissions": [ "core:default", "core:window:allow-start-dragging" - ], - "platforms": ["macOS"] + ] } From 0bb17f386cfe3731883231312f3ebefd5661c5dc Mon Sep 17 00:00:00 2001 From: max06bayer <101212752+max06bayer@users.noreply.github.com> Date: Mon, 20 Jul 2026 20:43:39 +0200 Subject: [PATCH 31/32] Link download button to latest release --- src/routes/+page.svelte | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 4c8a8ec..4233cbd 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -58,13 +58,13 @@

@@ -426,8 +426,7 @@ width: 192px; min-height: 38px; align-items: center; - justify-content: flex-start; - gap: 11px; + justify-content: center; padding: 0 10px; border: 1px solid transparent; border-radius: 12px; @@ -448,14 +447,6 @@ color: #111; } - .apple-mark { - width: 19px; - height: 19px; - align-self: center; - fill: currentColor; - transform: translateY(-1px); - } - .watch-link { position: relative; font-size: 16px; From 48431000748d8d3218520bf2c20d1b132e4964ba Mon Sep 17 00:00:00 2001 From: max06bayer <101212752+max06bayer@users.noreply.github.com> Date: Mon, 20 Jul 2026 23:30:31 +0200 Subject: [PATCH 32/32] landing page update --- src/lib/server/request-security.js | 3 +- src/routes/+page.svelte | 369 ++++++++++++----------------- 2 files changed, 159 insertions(+), 213 deletions(-) diff --git a/src/lib/server/request-security.js b/src/lib/server/request-security.js index 7991531..6c3284b 100644 --- a/src/lib/server/request-security.js +++ b/src/lib/server/request-security.js @@ -91,7 +91,8 @@ export function applySecurityHeaders(response, url) { 'Content-Security-Policy', "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'self'; form-action 'self'; " + "script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; " - + "img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self'; worker-src 'self' blob:; frame-src 'self' blob:" + + "img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self'; worker-src 'self' blob:; " + + "frame-src 'self' blob: https://www.youtube.com" ); if (url.protocol === 'https:' || env.NODE_ENV === 'production') { response.headers.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains'); diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 4233cbd..62e72d0 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -119,38 +119,14 @@ -