From 3cf435d59df9939223c3789fea81cb30bffebe24 Mon Sep 17 00:00:00 2001 From: alam00000 Date: Sat, 14 Feb 2026 21:38:58 +0530 Subject: [PATCH] feat: add custom branding, air-gapped deployment script, and updated self-hosting docs --- .env.example | 7 + .prettierignore | 2 + Dockerfile | 8 + Dockerfile.nonroot | 8 + README.md | 123 +++- docs/self-hosting/apache.md | 48 ++ docs/self-hosting/aws.md | 75 +- docs/self-hosting/cloudflare.md | 56 +- docs/self-hosting/docker.md | 17 + docs/self-hosting/index.md | 104 ++- docs/self-hosting/netlify.md | 75 +- docs/self-hosting/nginx.md | 70 +- docs/self-hosting/vercel.md | 65 +- public/locales/ar/common.json | 4 + public/locales/be/common.json | 4 + public/locales/da/common.json | 4 + public/locales/de/common.json | 4 + public/locales/en/common.json | 4 + public/locales/es/common.json | 4 + public/locales/fr/common.json | 4 + public/locales/id/common.json | 4 + public/locales/it/common.json | 4 + public/locales/nl/common.json | 4 + public/locales/pt/common.json | 4 + public/locales/tr/common.json | 4 + public/locales/vi/common.json | 4 + public/locales/zh-TW/common.json | 4 + public/locales/zh/common.json | 4 + scripts/prepare-airgap.sh | 755 +++++++++++++++++++++ simple-index.html | 19 +- src/js/main.ts | 16 +- src/js/workflow/nodes/divide-pages-node.ts | 43 +- src/pages/pdf-multi-tool.html | 8 +- src/partials/footer-simple.html | 14 +- src/partials/footer.html | 11 +- src/partials/navbar-simple.html | 11 +- src/partials/navbar.html | 11 +- vite.config.ts | 4 + 38 files changed, 1487 insertions(+), 123 deletions(-) create mode 100644 .prettierignore create mode 100755 scripts/prepare-airgap.sh diff --git a/.env.example b/.env.example index 7a79124..2cce17b 100644 --- a/.env.example +++ b/.env.example @@ -15,3 +15,10 @@ VITE_WASM_CPDF_URL=https://cdn.jsdelivr.net/npm/coherentpdf/dist/ # Default UI language (build-time) # Supported: en, ar, be, fr, de, es, zh, zh-TW, vi, tr, id, it, pt, nl, da VITE_DEFAULT_LANGUAGE= + +# Custom branding (build-time) +# Replace the default BentoPDF branding with your own. +# Place your logo file in the public/ folder and set the path relative to it. +VITE_BRAND_NAME= +VITE_BRAND_LOGO= +VITE_FOOTER_TEXT= diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..e70c3cd --- /dev/null +++ b/.prettierignore @@ -0,0 +1,2 @@ +# Handlebars partials with template syntax inside HTML attributes +src/partials/footer.html diff --git a/Dockerfile b/Dockerfile index 6ff58c6..af6375d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -39,6 +39,14 @@ ENV VITE_WASM_CPDF_URL=$VITE_WASM_CPDF_URL ARG VITE_DEFAULT_LANGUAGE ENV VITE_DEFAULT_LANGUAGE=$VITE_DEFAULT_LANGUAGE +# Custom branding (e.g. VITE_BRAND_NAME=MyCompany VITE_BRAND_LOGO=my-logo.svg) +ARG VITE_BRAND_NAME +ARG VITE_BRAND_LOGO +ARG VITE_FOOTER_TEXT +ENV VITE_BRAND_NAME=$VITE_BRAND_NAME +ENV VITE_BRAND_LOGO=$VITE_BRAND_LOGO +ENV VITE_FOOTER_TEXT=$VITE_FOOTER_TEXT + ENV NODE_OPTIONS="--max-old-space-size=3072" RUN npm run build:with-docs diff --git a/Dockerfile.nonroot b/Dockerfile.nonroot index 0cf3b6b..5581595 100644 --- a/Dockerfile.nonroot +++ b/Dockerfile.nonroot @@ -36,6 +36,14 @@ ENV VITE_WASM_CPDF_URL=$VITE_WASM_CPDF_URL ARG VITE_DEFAULT_LANGUAGE ENV VITE_DEFAULT_LANGUAGE=$VITE_DEFAULT_LANGUAGE +# Custom branding (e.g. VITE_BRAND_NAME=MyCompany VITE_BRAND_LOGO=my-logo.svg) +ARG VITE_BRAND_NAME +ARG VITE_BRAND_LOGO +ARG VITE_FOOTER_TEXT +ENV VITE_BRAND_NAME=$VITE_BRAND_NAME +ENV VITE_BRAND_LOGO=$VITE_BRAND_LOGO +ENV VITE_FOOTER_TEXT=$VITE_FOOTER_TEXT + ENV NODE_OPTIONS="--max-old-space-size=3072" RUN npm run build:with-docs diff --git a/README.md b/README.md index f280b2e..acd3d51 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ - [Docker Compose / Podman Compose](#-run-with-docker-compose--podman-compose-recommended) - [Podman Quadlet](#-podman-quadlet-systemd-integration) - [Simple Mode](#-simple-mode-for-internal-use) + - [Custom Branding](#-custom-branding) - [WASM Configuration](#wasm-configuration) - [Air-Gapped / Offline Deployment](#air-gapped--offline-deployment) - [Security Features](#-security-features) @@ -472,7 +473,69 @@ Users can also override these defaults per-browser via **Advanced Settings** in

🔒 Air-Gapped / Offline Deployment

-For networks with no internet access (government, healthcare, financial, etc.), you need to prepare everything on a machine **with** internet, then transfer it into the isolated network. +For networks with no internet access (government, healthcare, financial, etc.), you need to prepare everything on a machine **with** internet, then transfer the bundle into the isolated network. + +#### Automated Script (Recommended) + +The included `prepare-airgap.sh` script automates the entire process — downloading WASM packages, building the Docker image, exporting everything into a self-contained bundle with a setup script. + +```bash +git clone https://github.com/alam00000/bentopdf.git +cd bentopdf + +# Interactive mode — prompts for all options +bash scripts/prepare-airgap.sh + +# Or fully automated +bash scripts/prepare-airgap.sh --wasm-base-url https://internal.example.com/wasm +``` + +This produces a bundle directory containing: + +``` +bentopdf-airgap-bundle/ + bentopdf.tar # Docker image + *.tgz # WASM packages (PyMuPDF, Ghostscript, CoherentPDF) + setup.sh # Setup script for the air-gapped side + README.md # Instructions +``` + +**Transfer the bundle** into the air-gapped network via USB, internal artifact repo, or approved method. Then run the included setup script: + +```bash +cd bentopdf-airgap-bundle +bash setup.sh +``` + +The setup script loads the Docker image, extracts WASM files, and optionally starts the container. + +
+Script options + +| Flag | Description | Default | +| ----------------------- | ------------------------------------------------ | --------------------------------- | +| `--wasm-base-url ` | Where WASMs will be hosted internally | _(required, prompted if missing)_ | +| `--image-name ` | Docker image tag | `bentopdf` | +| `--output-dir ` | Output bundle directory | `./bentopdf-airgap-bundle` | +| `--simple-mode` | Enable Simple Mode | off | +| `--base-url ` | Subdirectory base URL (e.g. `/pdf/`) | `/` | +| `--language ` | Default UI language (e.g. `fr`, `de`) | _(none)_ | +| `--brand-name ` | Custom brand name | _(none)_ | +| `--brand-logo ` | Logo path relative to `public/` | _(none)_ | +| `--footer-text ` | Custom footer text | _(none)_ | +| `--dockerfile ` | Dockerfile to use | `Dockerfile` | +| `--skip-docker` | Skip Docker build and export | off | +| `--skip-wasm` | Skip WASM download (reuse existing `.tgz` files) | off | + +
+ +> [!IMPORTANT] +> WASM files must be served from the **same origin** as the BentoPDF app. Web Workers use `importScripts()` which cannot load scripts cross-origin. For example, if BentoPDF runs at `https://internal.example.com`, the WASM base URL should also be `https://internal.example.com/wasm`. + +#### Manual Steps + +
+If you prefer to do it manually without the script **Step 1: Download the WASM packages** (on a machine with internet) @@ -482,14 +545,9 @@ npm pack @bentopdf/gs-wasm npm pack coherentpdf ``` -This creates three `.tgz` files in your current directory. - -**Step 2: Build the Docker image with internal URLs** (on a machine with internet) - -Point the WASM URLs to where you'll host the files inside the air-gapped network: +**Step 2: Build the Docker image with internal URLs** ```bash -# Clone and build git clone https://github.com/alam00000/bentopdf.git cd bentopdf @@ -522,19 +580,18 @@ Copy these files via USB drive, internal artifact repository, or approved transf docker load -i bentopdf.tar # Extract the WASM packages -mkdir -p /var/www/wasm/pymupdf /var/www/wasm/gs /var/www/wasm/cpdf -tar xzf bentopdf-pymupdf-wasm-0.11.14.tgz -C /var/www/wasm/pymupdf --strip-components=1 -tar xzf bentopdf-gs-wasm-*.tgz -C /var/www/wasm/gs --strip-components=1 -tar xzf coherentpdf-*.tgz -C /var/www/wasm/cpdf --strip-components=1 - -# Serve the WASM files on your internal web server (e.g., nginx, Apache) -# Make sure they're accessible at the URLs you configured in Step 2 +mkdir -p ./wasm/pymupdf ./wasm/gs ./wasm/cpdf +tar xzf bentopdf-pymupdf-wasm-0.11.14.tgz -C ./wasm/pymupdf --strip-components=1 +tar xzf bentopdf-gs-wasm-*.tgz -C ./wasm/gs --strip-components=1 +tar xzf coherentpdf-*.tgz -C ./wasm/cpdf --strip-components=1 # Run BentoPDF docker run -d -p 3000:8080 --restart unless-stopped bentopdf ``` -Users open their browser, access BentoPDF on the internal network, and the browser fetches WASM files from the internal server. No internet required at any point. +Make sure the WASM files are accessible at the URLs you configured in Step 2. + +
> [!NOTE] > If you're building from source instead of Docker, set the variables in `.env.production` before running `npm run build`: @@ -684,6 +741,42 @@ For organizations that want a clean, distraction-free interface focused solely o For more details, see [SIMPLE_MODE.md](SIMPLE_MODE.md). +### 🎨 Custom Branding + +Replace the default BentoPDF logo, name, and footer text with your own. Branding is configured via environment variables at **build time** and works across all deployment methods (Docker, static hosting, air-gapped VMs). + +| Variable | Description | Default | +| ------------------ | --------------------------------------- | --------------------------------------- | +| `VITE_BRAND_NAME` | Brand name shown in header and footer | `BentoPDF` | +| `VITE_BRAND_LOGO` | Path to logo file relative to `public/` | `images/favicon-no-bg.svg` | +| `VITE_FOOTER_TEXT` | Custom footer/copyright text | `© 2026 BentoPDF. All rights reserved.` | + +**Docker:** + +```bash +docker build \ + --build-arg VITE_BRAND_NAME="AcmePDF" \ + --build-arg VITE_BRAND_LOGO="images/acme-logo.svg" \ + --build-arg VITE_FOOTER_TEXT="© 2026 Acme Corp. Internal use only." \ + -t acmepdf . +``` + +**Building from source:** + +Place your logo in the `public/` folder, then build: + +```bash +VITE_BRAND_NAME="AcmePDF" \ +VITE_BRAND_LOGO="images/acme-logo.svg" \ +VITE_FOOTER_TEXT="© 2026 Acme Corp. Internal use only." \ +npm run build +``` + +Or set the values in `.env.production` before building. + +> [!TIP] +> Branding works in both full mode and Simple Mode. You can combine it with other build-time options like `SIMPLE_MODE`, `BASE_URL`, and `VITE_DEFAULT_LANGUAGE`. + ### 🔒 Security Features BentoPDF runs as a non-root user using nginx-unprivileged for enhanced security: diff --git a/docs/self-hosting/apache.md b/docs/self-hosting/apache.md index 4b80551..bc9f000 100644 --- a/docs/self-hosting/apache.md +++ b/docs/self-hosting/apache.md @@ -17,6 +17,12 @@ npm install npm run build ``` +To customize branding, set environment variables before building: + +```bash +VITE_BRAND_NAME="AcmePDF" VITE_BRAND_LOGO="images/acme-logo.svg" npm run build +``` + ## Step 2: Copy Files ```bash @@ -52,6 +58,9 @@ Create `/etc/apache2/sites-available/bentopdf.conf`: # WASM MIME type AddType application/wasm .wasm + # Prevent double-compression of pre-compressed files + SetEnvIfNoCase Request_URI "\.gz$" no-gzip + # Compression AddOutputFilterByType DEFLATE text/html text/plain text/css application/javascript application/json application/wasm @@ -67,9 +76,24 @@ Create `/etc/apache2/sites-available/bentopdf.conf`: ExpiresByType image/svg+xml "access plus 1 year" + # Required headers for SharedArrayBuffer (LibreOffice WASM) + Header always set Cross-Origin-Embedder-Policy "require-corp" + Header always set Cross-Origin-Opener-Policy "same-origin" + Header always set Cross-Origin-Resource-Policy "cross-origin" + # Security headers Header always set X-Frame-Options "SAMEORIGIN" Header always set X-Content-Type-Options "nosniff" + + # Pre-compressed LibreOffice WASM files + + ForceType application/wasm + Header set Content-Encoding "gzip" + + + ForceType application/octet-stream + Header set Content-Encoding "gzip" + ``` @@ -191,6 +215,30 @@ Check that mod_rewrite is enabled: sudo a2enmod rewrite ``` +### Word/ODT/Excel to PDF Not Working + +LibreOffice WASM requires `SharedArrayBuffer`, which needs these headers: + +```apache +Header always set Cross-Origin-Embedder-Policy "require-corp" +Header always set Cross-Origin-Opener-Policy "same-origin" +``` + +The pre-compressed `.wasm.gz` and `.data.gz` files also need correct `Content-Encoding`: + +```apache + + ForceType application/wasm + Header set Content-Encoding "gzip" + + + ForceType application/octet-stream + Header set Content-Encoding "gzip" + +``` + +Ensure `mod_headers` is enabled: `sudo a2enmod headers` + ### Permission Denied ```bash diff --git a/docs/self-hosting/aws.md b/docs/self-hosting/aws.md index 43e0e4d..7d18dee 100644 --- a/docs/self-hosting/aws.md +++ b/docs/self-hosting/aws.md @@ -23,7 +23,8 @@ aws s3 website s3://your-bentopdf-bucket \ ## Step 2: Build and Upload ```bash -# Build the project +# Build the project (optionally with custom branding) +# VITE_BRAND_NAME="AcmePDF" VITE_BRAND_LOGO="images/acme-logo.svg" npm run build npm run build # Sync to S3 @@ -56,6 +57,62 @@ Or use the AWS Console: 4. Default root object: `index.html` 5. Create distribution +## Step 3b: Response Headers Policy (Required for LibreOffice WASM) + +LibreOffice-based conversions (Word, Excel, PowerPoint to PDF) require `SharedArrayBuffer`, which needs specific response headers. Create a CloudFront Response Headers Policy: + +1. Go to CloudFront → Policies → Response headers +2. Create a custom policy with these headers: + +| Header | Value | +| ------------------------------ | -------------- | +| `Cross-Origin-Embedder-Policy` | `require-corp` | +| `Cross-Origin-Opener-Policy` | `same-origin` | +| `Cross-Origin-Resource-Policy` | `cross-origin` | + +3. Attach the policy to your distribution's default cache behavior + +Or via CLI: + +```bash +aws cloudfront create-response-headers-policy \ + --response-headers-policy-config '{ + "Name": "BentoPDF-COEP-COOP", + "CustomHeadersConfig": { + "Quantity": 3, + "Items": [ + {"Header": "Cross-Origin-Embedder-Policy", "Value": "require-corp", "Override": true}, + {"Header": "Cross-Origin-Opener-Policy", "Value": "same-origin", "Override": true}, + {"Header": "Cross-Origin-Resource-Policy", "Value": "cross-origin", "Override": true} + ] + } + }' +``` + +## Step 3c: S3 Metadata for Pre-Compressed WASM Files + +The LibreOffice WASM files are pre-compressed (`.wasm.gz`, `.data.gz`). Set the correct Content-Type and Content-Encoding so browsers decompress them: + +```bash +# Set correct headers for soffice.wasm.gz +aws s3 cp s3://your-bentopdf-bucket/libreoffice-wasm/soffice.wasm.gz \ + s3://your-bentopdf-bucket/libreoffice-wasm/soffice.wasm.gz \ + --content-type "application/wasm" \ + --content-encoding "gzip" \ + --metadata-directive REPLACE + +# Set correct headers for soffice.data.gz +aws s3 cp s3://your-bentopdf-bucket/libreoffice-wasm/soffice.data.gz \ + s3://your-bentopdf-bucket/libreoffice-wasm/soffice.data.gz \ + --content-type "application/octet-stream" \ + --content-encoding "gzip" \ + --metadata-directive REPLACE +``` + +::: warning Important +Without the response headers policy, `SharedArrayBuffer` is unavailable and LibreOffice WASM conversions will hang at ~55%. Without the correct Content-Encoding on the `.gz` files, the browser receives raw gzip bytes and WASM compilation fails. +::: + ## Step 4: S3 Bucket Policy Allow CloudFront to access the bucket: @@ -94,11 +151,11 @@ Configure 404 to return `index.html` for SPA routing: ## Cost Estimation -| Resource | Estimated Cost | -|----------|----------------| -| S3 Storage (~500MB) | ~$0.01/month | -| CloudFront (1TB transfer) | ~$85/month | -| CloudFront (10GB transfer) | ~$0.85/month | +| Resource | Estimated Cost | +| -------------------------- | -------------- | +| S3 Storage (~500MB) | ~$0.01/month | +| CloudFront (1TB transfer) | ~$85/month | +| CloudFront (10GB transfer) | ~$0.85/month | ::: tip Use S3 Intelligent Tiering for cost optimization on infrequently accessed files. @@ -117,15 +174,15 @@ resource "aws_cloudfront_distribution" "bentopdf" { domain_name = aws_s3_bucket.bentopdf.bucket_regional_domain_name origin_id = "S3Origin" } - + enabled = true default_root_object = "index.html" - + default_cache_behavior { allowed_methods = ["GET", "HEAD"] cached_methods = ["GET", "HEAD"] target_origin_id = "S3Origin" - + viewer_protocol_policy = "redirect-to-https" } } diff --git a/docs/self-hosting/cloudflare.md b/docs/self-hosting/cloudflare.md index 0d8850f..3f9ca7b 100644 --- a/docs/self-hosting/cloudflare.md +++ b/docs/self-hosting/cloudflare.md @@ -10,27 +10,49 @@ ## Build Configuration -| Setting | Value | -|---------|-------| -| Framework preset | None | -| Build command | `npm run build` | -| Build output directory | `dist` | -| Root directory | `/` | +| Setting | Value | +| ---------------------- | --------------- | +| Framework preset | None | +| Build command | `npm run build` | +| Build output directory | `dist` | +| Root directory | `/` | ## Environment Variables Add these in Settings → Environment variables: -| Variable | Value | -|----------|-------| -| `NODE_VERSION` | `18` | -| `SIMPLE_MODE` | `false` (optional) | +| Variable | Value | +| ----------------------- | ------------------------------------------ | +| `NODE_VERSION` | `18` | +| `SIMPLE_MODE` | `false` (optional) | +| `VITE_BRAND_NAME` | Custom brand name (optional) | +| `VITE_BRAND_LOGO` | Logo path relative to `public/` (optional) | +| `VITE_FOOTER_TEXT` | Custom footer/copyright text (optional) | +| `VITE_DEFAULT_LANGUAGE` | Default UI language, e.g. `fr` (optional) | ## Configuration File Create `_headers` in your `public` folder: ``` +# Required security headers for SharedArrayBuffer (used by LibreOffice WASM) +/* + Cross-Origin-Embedder-Policy: require-corp + Cross-Origin-Opener-Policy: same-origin + Cross-Origin-Resource-Policy: cross-origin + +# Pre-compressed LibreOffice WASM binary +/libreoffice-wasm/soffice.wasm.gz + Content-Type: application/wasm + Content-Encoding: gzip + Cache-Control: public, max-age=31536000, immutable + +# Pre-compressed LibreOffice WASM data +/libreoffice-wasm/soffice.data.gz + Content-Type: application/octet-stream + Content-Encoding: gzip + Cache-Control: public, max-age=31536000, immutable + # Cache WASM files aggressively /*.wasm Cache-Control: public, max-age=31536000, immutable @@ -41,6 +63,10 @@ Create `_headers` in your `public` folder: Cache-Control: no-cache ``` +::: warning Important +The `Cross-Origin-Embedder-Policy` and `Cross-Origin-Opener-Policy` headers are required for Word/ODT/Excel/PowerPoint to PDF conversions. Without them, `SharedArrayBuffer` is unavailable and the LibreOffice WASM engine will fail to initialize. +::: + Create `_redirects` for SPA routing: ``` @@ -89,11 +115,11 @@ npx wrangler deploy ### Security Features -| Feature | Description | -|---------|-------------| -| **URL Restrictions** | Only certificate URLs allowed | -| **File Size Limit** | Max 10MB per request | -| **Rate Limiting** | 60 req/IP/min (requires KV) | +| Feature | Description | +| ----------------------- | ------------------------------ | +| **URL Restrictions** | Only certificate URLs allowed | +| **File Size Limit** | Max 10MB per request | +| **Rate Limiting** | 60 req/IP/min (requires KV) | | **Private IP Blocking** | Blocks localhost, internal IPs | ### Enable Rate Limiting diff --git a/docs/self-hosting/docker.md b/docs/self-hosting/docker.md index fadd5f9..2ce4f7f 100644 --- a/docs/self-hosting/docker.md +++ b/docs/self-hosting/docker.md @@ -96,6 +96,9 @@ docker run -d -p 3000:8080 bentopdf:custom | `VITE_WASM_GS_URL` | Ghostscript WASM module URL | `https://cdn.jsdelivr.net/npm/@bentopdf/gs-wasm/assets/` | | `VITE_WASM_CPDF_URL` | CoherentPDF WASM module URL | `https://cdn.jsdelivr.net/npm/coherentpdf/dist/` | | `VITE_DEFAULT_LANGUAGE` | Default UI language | `en` | +| `VITE_BRAND_NAME` | Custom brand name | `BentoPDF` | +| `VITE_BRAND_LOGO` | Logo path relative to `public/` | `images/favicon-no-bg.svg` | +| `VITE_FOOTER_TEXT` | Custom footer/copyright text | `© 2026 BentoPDF. All rights reserved.` | WASM module URLs are pre-configured with CDN defaults — all advanced features work out of the box. Override these for air-gapped or self-hosted deployments. @@ -109,6 +112,20 @@ docker build --build-arg VITE_DEFAULT_LANGUAGE=fr -t bentopdf . docker run -d -p 3000:8080 bentopdf ``` +### Custom Branding + +Replace the default BentoPDF logo, name, and footer text with your own. Place your logo file in the `public/` folder (or use an existing image), then pass the branding variables at build time: + +```bash +docker build \ + --build-arg VITE_BRAND_NAME="AcmePDF" \ + --build-arg VITE_BRAND_LOGO="images/acme-logo.svg" \ + --build-arg VITE_FOOTER_TEXT="© 2026 Acme Corp. Internal use only." \ + -t acmepdf . +``` + +Branding works in both full mode and Simple Mode, and can be combined with all other build-time options. + ### Custom WASM URLs (Air-Gapped / Self-Hosted) > [!IMPORTANT] diff --git a/docs/self-hosting/index.md b/docs/self-hosting/index.md index 260ebd1..a08c18c 100644 --- a/docs/self-hosting/index.md +++ b/docs/self-hosting/index.md @@ -103,6 +103,36 @@ Deploy to a subdirectory: BASE_URL=/pdf-tools/ npm run build ``` +### Custom Branding + +Replace the default BentoPDF logo, name, and footer text with your own at build time: + +| Variable | Description | Default | +| ------------------ | ------------------------------------- | --------------------------------------- | +| `VITE_BRAND_NAME` | Brand name shown in header and footer | `BentoPDF` | +| `VITE_BRAND_LOGO` | Logo path relative to `public/` | `images/favicon-no-bg.svg` | +| `VITE_FOOTER_TEXT` | Custom footer/copyright text | `© 2026 BentoPDF. All rights reserved.` | + +```bash +# Place your logo in public/, then build +VITE_BRAND_NAME="AcmePDF" \ +VITE_BRAND_LOGO="images/acme-logo.svg" \ +VITE_FOOTER_TEXT="© 2026 Acme Corp. Internal use only." \ +npm run build +``` + +Or via Docker: + +```bash +docker build \ + --build-arg VITE_BRAND_NAME="AcmePDF" \ + --build-arg VITE_BRAND_LOGO="images/acme-logo.svg" \ + --build-arg VITE_FOOTER_TEXT="© 2026 Acme Corp. Internal use only." \ + -t acmepdf . +``` + +Branding works in both full mode and Simple Mode, and can be combined with all other build-time options (`BASE_URL`, `SIMPLE_MODE`, `VITE_DEFAULT_LANGUAGE`). + ## Deployment Guides Choose your platform: @@ -166,6 +196,66 @@ Users can also override these defaults at any time via **Advanced Settings** in For networks with no internet access (government, healthcare, financial, etc.). The WASM URLs are baked into the JavaScript at **build time** — the actual WASM files are downloaded by the **user's browser** at runtime. So you need to prepare everything on a machine with internet, then transfer it into the isolated network. +#### Automated Script (Recommended) + +The included `prepare-airgap.sh` script automates the entire process — downloading WASM packages, building the Docker image, and producing a self-contained bundle with a setup script. + +```bash +git clone https://github.com/alam00000/bentopdf.git +cd bentopdf + +# Interactive mode — prompts for all options +bash scripts/prepare-airgap.sh + +# Or fully automated +bash scripts/prepare-airgap.sh --wasm-base-url https://internal.example.com/wasm +``` + +This produces a bundle directory: + +``` +bentopdf-airgap-bundle/ + bentopdf.tar # Docker image + *.tgz # WASM packages (PyMuPDF, Ghostscript, CoherentPDF) + setup.sh # Setup script for the air-gapped side + README.md # Instructions +``` + +Transfer the bundle into the air-gapped network via USB, internal artifact repo, or approved method. Then run the included setup script: + +```bash +cd bentopdf-airgap-bundle +bash setup.sh +``` + +The setup script loads the Docker image, extracts WASM files, and optionally starts the container. + +**Script options:** + +| Flag | Description | Default | +| ----------------------- | ------------------------------------------------ | --------------------------------- | +| `--wasm-base-url ` | Where WASMs will be hosted internally | _(required, prompted if missing)_ | +| `--image-name ` | Docker image tag | `bentopdf` | +| `--output-dir ` | Output bundle directory | `./bentopdf-airgap-bundle` | +| `--simple-mode` | Enable Simple Mode | off | +| `--base-url ` | Subdirectory base URL (e.g. `/pdf/`) | `/` | +| `--language ` | Default UI language (e.g. `fr`, `de`) | _(none)_ | +| `--brand-name ` | Custom brand name | _(none)_ | +| `--brand-logo ` | Logo path relative to `public/` | _(none)_ | +| `--footer-text ` | Custom footer text | _(none)_ | +| `--dockerfile ` | Dockerfile to use | `Dockerfile` | +| `--skip-docker` | Skip Docker build and export | off | +| `--skip-wasm` | Skip WASM download (reuse existing `.tgz` files) | off | + +::: warning Same-Origin Requirement +WASM files must be served from the **same origin** as the BentoPDF app. Web Workers use `importScripts()` which cannot load scripts cross-origin. For example, if BentoPDF runs at `https://internal.example.com`, the WASM base URL should also be `https://internal.example.com/wasm`. +::: + +#### Manual Steps + +
+If you prefer to do it manually without the script + **Step 1: Download the WASM packages** (on a machine with internet) ```bash @@ -206,17 +296,19 @@ Copy via USB, internal artifact repo, or approved transfer method: # Load the Docker image docker load -i bentopdf.tar -# Extract WASM packages to your internal web server's document root -mkdir -p /var/www/wasm/pymupdf /var/www/wasm/gs /var/www/wasm/cpdf -tar xzf bentopdf-pymupdf-wasm-0.11.14.tgz -C /var/www/wasm/pymupdf --strip-components=1 -tar xzf bentopdf-gs-wasm-*.tgz -C /var/www/wasm/gs --strip-components=1 -tar xzf coherentpdf-*.tgz -C /var/www/wasm/cpdf --strip-components=1 +# Extract WASM packages +mkdir -p ./wasm/pymupdf ./wasm/gs ./wasm/cpdf +tar xzf bentopdf-pymupdf-wasm-0.11.14.tgz -C ./wasm/pymupdf --strip-components=1 +tar xzf bentopdf-gs-wasm-*.tgz -C ./wasm/gs --strip-components=1 +tar xzf coherentpdf-*.tgz -C ./wasm/cpdf --strip-components=1 # Run BentoPDF docker run -d -p 3000:8080 --restart unless-stopped bentopdf ``` -Make sure the WASM files are accessible at the URLs you configured in Step 2. Users open their browser and everything works — no internet required. +Make sure the WASM files are accessible at the URLs you configured in Step 2. + +
::: info Building from source instead of Docker? Set the variables in `.env.production` before running `npm run build`: diff --git a/docs/self-hosting/netlify.md b/docs/self-hosting/netlify.md index 910fa23..c058566 100644 --- a/docs/self-hosting/netlify.md +++ b/docs/self-hosting/netlify.md @@ -17,11 +17,11 @@ ### Step 2: Configure Build Settings -| Setting | Value | -|---------|-------| -| Build command | `npm run build` | -| Publish directory | `dist` | -| Node version | 18+ | +| Setting | Value | +| ----------------- | --------------- | +| Build command | `npm run build` | +| Publish directory | `dist` | +| Node version | 18+ | ### Step 3: Deploy @@ -39,27 +39,61 @@ Create `netlify.toml` in your project root: [build.environment] NODE_VERSION = "18" -# SPA routing -[[redirects]] - from = "/*" - to = "/index.html" - status = 200 +# Required security headers for SharedArrayBuffer (used by LibreOffice WASM) +[[headers]] + for = "/*" + [headers.values] + Cross-Origin-Embedder-Policy = "require-corp" + Cross-Origin-Opener-Policy = "same-origin" + Cross-Origin-Resource-Policy = "cross-origin" -# Cache WASM files +# Pre-compressed LibreOffice WASM binary - must be served with Content-Encoding +[[headers]] + for = "/libreoffice-wasm/soffice.wasm.gz" + [headers.values] + Content-Type = "application/wasm" + Content-Encoding = "gzip" + Cache-Control = "public, max-age=31536000, immutable" + +# Pre-compressed LibreOffice WASM data - must be served with Content-Encoding +[[headers]] + for = "/libreoffice-wasm/soffice.data.gz" + [headers.values] + Content-Type = "application/octet-stream" + Content-Encoding = "gzip" + Cache-Control = "public, max-age=31536000, immutable" + +# Cache other WASM files [[headers]] for = "*.wasm" [headers.values] Cache-Control = "public, max-age=31536000, immutable" Content-Type = "application/wasm" + +# SPA routing +[[redirects]] + from = "/*" + to = "/index.html" + status = 200 ``` +::: warning Important +The `Cross-Origin-Embedder-Policy` and `Cross-Origin-Opener-Policy` headers are required for Word/ODT/Excel/PowerPoint to PDF conversions. Without them, `SharedArrayBuffer` is unavailable and the LibreOffice WASM engine will fail to initialize. + +The `Content-Encoding: gzip` headers on the `.wasm.gz` and `.data.gz` files tell the browser to decompress them automatically. Without these, the browser receives raw gzip bytes and WASM compilation fails. +::: + ## Environment Variables Set these in Site settings → Environment variables: -| Variable | Description | -|----------|-------------| -| `SIMPLE_MODE` | Set to `true` for minimal build | +| Variable | Description | +| ----------------------- | ----------------------------------------------------------- | +| `SIMPLE_MODE` | Set to `true` for minimal build | +| `VITE_BRAND_NAME` | Custom brand name (replaces "BentoPDF") | +| `VITE_BRAND_LOGO` | Logo path relative to `public/` (e.g. `images/my-logo.svg`) | +| `VITE_FOOTER_TEXT` | Custom footer/copyright text | +| `VITE_DEFAULT_LANGUAGE` | Default UI language (e.g. `fr`, `de`, `es`) | ## Custom Domain @@ -78,6 +112,19 @@ git lfs track "*.wasm" ## Troubleshooting +### Word/ODT/Excel to PDF Stuck at 55% + +If document conversions hang at 55%, open DevTools Console and check: + +```js +console.log(window.crossOriginIsolated); // should be true +console.log(typeof SharedArrayBuffer); // should be "function" +``` + +If `crossOriginIsolated` is `false`, the COEP/COOP headers from your `netlify.toml` are not being applied. Make sure the file is in your project root and redeploy. + +If you see `expected magic word 00 61 73 6d, found 1f 8b 08 08` in the console, the `.wasm.gz` files are missing `Content-Encoding: gzip` headers. Ensure the `[[headers]]` blocks for `soffice.wasm.gz` and `soffice.data.gz` are in your `netlify.toml`. + ### Build Fails Check Node version compatibility: diff --git a/docs/self-hosting/nginx.md b/docs/self-hosting/nginx.md index 35b0612..79c666b 100644 --- a/docs/self-hosting/nginx.md +++ b/docs/self-hosting/nginx.md @@ -17,6 +17,12 @@ npm install npm run build ``` +To customize branding, set environment variables before building: + +```bash +VITE_BRAND_NAME="AcmePDF" VITE_BRAND_LOGO="images/acme-logo.svg" npm run build +``` + ## Step 2: Copy Files ```bash @@ -57,21 +63,51 @@ server { application/wasm wasm; } - # Cache static assets - location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|wasm)$ { - expires 1y; - add_header Cache-Control "public, immutable"; - } - - # SPA routing - serve index.html for all routes - location / { - try_files $uri $uri/ /index.html; - } + # Required headers for SharedArrayBuffer (LibreOffice WASM) + # These must be on every response - especially HTML pages + add_header Cross-Origin-Embedder-Policy "require-corp" always; + add_header Cross-Origin-Opener-Policy "same-origin" always; + add_header Cross-Origin-Resource-Policy "cross-origin" always; # Security headers add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header X-XSS-Protection "1; mode=block" always; + + # Pre-compressed LibreOffice WASM binary + location ~* /libreoffice-wasm/soffice\.wasm\.gz$ { + gzip off; + types {} default_type application/wasm; + add_header Content-Encoding gzip; + add_header Cache-Control "public, immutable"; + add_header Cross-Origin-Embedder-Policy "require-corp" always; + add_header Cross-Origin-Opener-Policy "same-origin" always; + } + + # Pre-compressed LibreOffice WASM data + location ~* /libreoffice-wasm/soffice\.data\.gz$ { + gzip off; + types {} default_type application/octet-stream; + add_header Content-Encoding gzip; + add_header Cache-Control "public, immutable"; + add_header Cross-Origin-Embedder-Policy "require-corp" always; + add_header Cross-Origin-Opener-Policy "same-origin" always; + } + + # Cache static assets + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|wasm)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + add_header Cross-Origin-Embedder-Policy "require-corp" always; + add_header Cross-Origin-Opener-Policy "same-origin" always; + } + + # SPA routing - serve index.html for all routes + location / { + try_files $uri $uri/ /index.html; + add_header Cross-Origin-Embedder-Policy "require-corp" always; + add_header Cross-Origin-Opener-Policy "same-origin" always; + } } ``` @@ -120,7 +156,7 @@ http { # Increase buffer sizes client_max_body_size 100M; - + # Worker connections worker_connections 2048; } @@ -138,6 +174,18 @@ types { } ``` +### Word/ODT/Excel to PDF Not Working + +LibreOffice WASM requires `SharedArrayBuffer`, which needs `Cross-Origin-Embedder-Policy` and `Cross-Origin-Opener-Policy` headers. Note that nginx `add_header` directives in a `location` block **override** server-level `add_header` directives — they don't merge. Every `location` block with its own `add_header` must include the COEP/COOP headers. + +Verify with: + +```bash +curl -I https://your-domain.com/ | grep -i cross-origin +``` + +If using a reverse proxy in front of nginx, ensure it preserves these headers. + ### 502 Bad Gateway Check Nginx error logs: diff --git a/docs/self-hosting/vercel.md b/docs/self-hosting/vercel.md index ed0b9e5..e8d55da 100644 --- a/docs/self-hosting/vercel.md +++ b/docs/self-hosting/vercel.md @@ -18,20 +18,24 @@ Fork [bentopdf/bentopdf](https://github.com/alam00000/bentopdf) to your GitHub a 2. Select your forked repository 3. Configure the project: -| Setting | Value | -|---------|-------| -| Framework Preset | Vite | -| Build Command | `npm run build` | -| Output Directory | `dist` | -| Install Command | `npm install` | +| Setting | Value | +| ---------------- | --------------- | +| Framework Preset | Vite | +| Build Command | `npm run build` | +| Output Directory | `dist` | +| Install Command | `npm install` | ### Step 3: Environment Variables (Optional) Add these if needed: -``` -SIMPLE_MODE=false -``` +| Variable | Description | +| ----------------------- | ----------------------------------------------------------- | +| `SIMPLE_MODE` | Set to `true` for minimal UI | +| `VITE_BRAND_NAME` | Custom brand name (replaces "BentoPDF") | +| `VITE_BRAND_LOGO` | Logo path relative to `public/` (e.g. `images/my-logo.svg`) | +| `VITE_FOOTER_TEXT` | Custom footer/copyright text | +| `VITE_DEFAULT_LANGUAGE` | Default UI language (e.g. `fr`, `de`, `es`) | ### Step 4: Deploy @@ -73,3 +77,46 @@ Add a `vercel.json` for SPA routing: "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }] } ``` + +### Word/ODT/Excel to PDF Not Working + +LibreOffice WASM conversions require `SharedArrayBuffer`, which needs these response headers on all pages: + +- `Cross-Origin-Embedder-Policy: require-corp` +- `Cross-Origin-Opener-Policy: same-origin` + +The pre-compressed `.wasm.gz` and `.data.gz` files also need `Content-Encoding: gzip` so the browser decompresses them. + +Add these to your `vercel.json`: + +```json +{ + "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }], + "headers": [ + { + "source": "/(.*)", + "headers": [ + { "key": "Cross-Origin-Embedder-Policy", "value": "require-corp" }, + { "key": "Cross-Origin-Opener-Policy", "value": "same-origin" }, + { "key": "Cross-Origin-Resource-Policy", "value": "cross-origin" } + ] + }, + { + "source": "/libreoffice-wasm/soffice.wasm.gz", + "headers": [ + { "key": "Content-Type", "value": "application/wasm" }, + { "key": "Content-Encoding", "value": "gzip" } + ] + }, + { + "source": "/libreoffice-wasm/soffice.data.gz", + "headers": [ + { "key": "Content-Type", "value": "application/octet-stream" }, + { "key": "Content-Encoding", "value": "gzip" } + ] + } + ] +} +``` + +To verify, open DevTools Console and run `console.log(window.crossOriginIsolated)` — it should return `true`. diff --git a/public/locales/ar/common.json b/public/locales/ar/common.json index e8da0b7..133e022 100644 --- a/public/locales/ar/common.json +++ b/public/locales/ar/common.json @@ -357,5 +357,9 @@ "errorRendering": "فشل عرض الصور المصغرة للصفحات", "error": "خطأ", "failedToLoad": "فشل التحميل" + }, + "simpleMode": { + "title": "أدوات PDF", + "subtitle": "اختر أداة للبدء" } } diff --git a/public/locales/be/common.json b/public/locales/be/common.json index 7376314..438dea7 100644 --- a/public/locales/be/common.json +++ b/public/locales/be/common.json @@ -355,5 +355,9 @@ }, "relatedTools": { "title": "Звязаныя інструменты PDF" + }, + "simpleMode": { + "title": "Інструменты PDF", + "subtitle": "Абярыце інструмент, каб пачаць" } } diff --git a/public/locales/da/common.json b/public/locales/da/common.json index f8e098d..6710815 100644 --- a/public/locales/da/common.json +++ b/public/locales/da/common.json @@ -357,5 +357,9 @@ "errorRendering": "Kunne ikke rendere side-miniaturer", "error": "Fejl", "failedToLoad": "Kunne ikke indlæses" + }, + "simpleMode": { + "title": "PDF-værktøjer", + "subtitle": "Vælg et værktøj for at komme i gang" } } diff --git a/public/locales/de/common.json b/public/locales/de/common.json index cb39144..70ec209 100644 --- a/public/locales/de/common.json +++ b/public/locales/de/common.json @@ -357,5 +357,9 @@ }, "relatedTools": { "title": "Verwandte PDF-Tools" + }, + "simpleMode": { + "title": "PDF-Werkzeuge", + "subtitle": "Wählen Sie ein Werkzeug aus, um zu beginnen" } } diff --git a/public/locales/en/common.json b/public/locales/en/common.json index aaf1455..475ee64 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -357,5 +357,9 @@ "errorRendering": "Failed to render page thumbnails", "error": "Error", "failedToLoad": "Failed to load" + }, + "simpleMode": { + "title": "PDF Tools", + "subtitle": "Select a tool to get started" } } diff --git a/public/locales/es/common.json b/public/locales/es/common.json index 71e0f3a..690ba7b 100644 --- a/public/locales/es/common.json +++ b/public/locales/es/common.json @@ -357,5 +357,9 @@ }, "relatedTools": { "title": "Herramientas PDF relacionadas" + }, + "simpleMode": { + "title": "Herramientas PDF", + "subtitle": "Selecciona una herramienta para comenzar" } } diff --git a/public/locales/fr/common.json b/public/locales/fr/common.json index a1054ab..cbd7211 100644 --- a/public/locales/fr/common.json +++ b/public/locales/fr/common.json @@ -357,5 +357,9 @@ }, "relatedTools": { "title": "Outils PDF associés" + }, + "simpleMode": { + "title": "Outils PDF", + "subtitle": "Sélectionnez un outil pour commencer" } } diff --git a/public/locales/id/common.json b/public/locales/id/common.json index 6fbc93c..f178594 100644 --- a/public/locales/id/common.json +++ b/public/locales/id/common.json @@ -357,5 +357,9 @@ }, "relatedTools": { "title": "Alat PDF Terkait" + }, + "simpleMode": { + "title": "Alat PDF", + "subtitle": "Pilih alat untuk memulai" } } diff --git a/public/locales/it/common.json b/public/locales/it/common.json index 573a9d4..e938a8a 100644 --- a/public/locales/it/common.json +++ b/public/locales/it/common.json @@ -357,5 +357,9 @@ }, "relatedTools": { "title": "Strumenti PDF correlati" + }, + "simpleMode": { + "title": "Strumenti PDF", + "subtitle": "Seleziona uno strumento per iniziare" } } diff --git a/public/locales/nl/common.json b/public/locales/nl/common.json index 27cfb70..348ada3 100644 --- a/public/locales/nl/common.json +++ b/public/locales/nl/common.json @@ -357,5 +357,9 @@ }, "relatedTools": { "title": "Gerelateerde PDF-tools" + }, + "simpleMode": { + "title": "PDF-tools", + "subtitle": "Selecteer een tool om te beginnen" } } diff --git a/public/locales/pt/common.json b/public/locales/pt/common.json index 04879ed..632ebb9 100644 --- a/public/locales/pt/common.json +++ b/public/locales/pt/common.json @@ -357,5 +357,9 @@ }, "relatedTools": { "title": "Ferramentas PDF relacionadas" + }, + "simpleMode": { + "title": "Ferramentas PDF", + "subtitle": "Selecione uma ferramenta para começar" } } diff --git a/public/locales/tr/common.json b/public/locales/tr/common.json index d344824..ca6fceb 100644 --- a/public/locales/tr/common.json +++ b/public/locales/tr/common.json @@ -357,5 +357,9 @@ }, "relatedTools": { "title": "İlgili PDF Araçları" + }, + "simpleMode": { + "title": "PDF Araçları", + "subtitle": "Başlamak için bir araç seçin" } } diff --git a/public/locales/vi/common.json b/public/locales/vi/common.json index a778bff..0a57f66 100644 --- a/public/locales/vi/common.json +++ b/public/locales/vi/common.json @@ -357,5 +357,9 @@ }, "relatedTools": { "title": "Công cụ PDF liên quan" + }, + "simpleMode": { + "title": "Công cụ PDF", + "subtitle": "Chọn một công cụ để bắt đầu" } } diff --git a/public/locales/zh-TW/common.json b/public/locales/zh-TW/common.json index ea07a4d..7b10dc4 100644 --- a/public/locales/zh-TW/common.json +++ b/public/locales/zh-TW/common.json @@ -357,5 +357,9 @@ }, "relatedTools": { "title": "相關 PDF 工具" + }, + "simpleMode": { + "title": "PDF 工具", + "subtitle": "選擇一個工具開始使用" } } diff --git a/public/locales/zh/common.json b/public/locales/zh/common.json index 9458f95..a3b76bd 100644 --- a/public/locales/zh/common.json +++ b/public/locales/zh/common.json @@ -357,5 +357,9 @@ }, "relatedTools": { "title": "相关 PDF 工具" + }, + "simpleMode": { + "title": "PDF 工具", + "subtitle": "选择一个工具开始使用" } } diff --git a/scripts/prepare-airgap.sh b/scripts/prepare-airgap.sh new file mode 100755 index 0000000..b87d54c --- /dev/null +++ b/scripts/prepare-airgap.sh @@ -0,0 +1,755 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ============================================================ +# BentoPDF Air-Gapped Deployment Preparation Script +# ============================================================ +# Automates the creation of a self-contained deployment bundle +# for air-gapped (offline) networks. +# +# Run this on a machine WITH internet access. The output bundle +# contains everything needed to deploy BentoPDF offline. +# +# Usage: +# bash scripts/prepare-airgap.sh --wasm-base-url https://internal.example.com/wasm +# bash scripts/prepare-airgap.sh # interactive mode +# +# See --help for all options. +# ============================================================ + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +# --- Output formatting --- +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +BOLD='\033[1m' +NC='\033[0m' + +info() { echo -e "${BLUE}[INFO]${NC} $*"; } +success() { echo -e "${GREEN}[OK]${NC} $*"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } +error() { echo -e "${RED}[ERR]${NC} $*" >&2; } +step() { echo -e "\n${BOLD}==> $*${NC}"; } + +# Disable colors if NO_COLOR is set +if [ -n "${NO_COLOR:-}" ]; then + RED='' GREEN='' YELLOW='' BLUE='' BOLD='' NC='' +fi + +# --- Defaults --- +WASM_BASE_URL="" +IMAGE_NAME="bentopdf" +OUTPUT_DIR="./bentopdf-airgap-bundle" +SIMPLE_MODE="" +BASE_URL="" +COMPRESSION_MODE="" +LANGUAGE="" +BRAND_NAME="" +BRAND_LOGO="" +FOOTER_TEXT="" +DOCKERFILE="Dockerfile" +SKIP_DOCKER=false +SKIP_WASM=false +INTERACTIVE=false + +# --- Usage --- +usage() { + cat <<'EOF' +BentoPDF Air-Gapped Deployment Preparation + +USAGE: + bash scripts/prepare-airgap.sh [OPTIONS] + bash scripts/prepare-airgap.sh # interactive mode + +REQUIRED: + --wasm-base-url Base URL where WASM files will be hosted + in the air-gapped network + (e.g. https://internal.example.com/wasm) + +OPTIONS: + --image-name Docker image name (default: bentopdf) + --output-dir Output bundle directory (default: ./bentopdf-airgap-bundle) + --dockerfile Dockerfile to use (default: Dockerfile) + --simple-mode Enable Simple Mode + --base-url Subdirectory base URL (e.g. /pdf/) + --compression Compression: g, b, o, all (default: all) + --language Default UI language (e.g. fr, de, es) + --brand-name Custom brand name + --brand-logo Logo path relative to public/ + --footer-text Custom footer text + --skip-docker Skip Docker build and export + --skip-wasm Skip WASM download (reuse existing .tgz files) + --help Show this help message + +EXAMPLES: + # Minimal (prompts for WASM URL interactively) + bash scripts/prepare-airgap.sh + + # Full automation + bash scripts/prepare-airgap.sh \ + --wasm-base-url https://internal.example.com/wasm \ + --brand-name "AcmePDF" \ + --language fr + + # Skip Docker build (reuse existing image) + bash scripts/prepare-airgap.sh \ + --wasm-base-url https://internal.example.com/wasm \ + --skip-docker +EOF + exit 0 +} + +# --- Parse arguments --- +while [[ $# -gt 0 ]]; do + case "$1" in + --wasm-base-url) WASM_BASE_URL="$2"; shift 2 ;; + --image-name) IMAGE_NAME="$2"; shift 2 ;; + --output-dir) OUTPUT_DIR="$2"; shift 2 ;; + --simple-mode) SIMPLE_MODE="true"; shift ;; + --base-url) BASE_URL="$2"; shift 2 ;; + --compression) COMPRESSION_MODE="$2"; shift 2 ;; + --language) LANGUAGE="$2"; shift 2 ;; + --brand-name) BRAND_NAME="$2"; shift 2 ;; + --brand-logo) BRAND_LOGO="$2"; shift 2 ;; + --footer-text) FOOTER_TEXT="$2"; shift 2 ;; + --dockerfile) DOCKERFILE="$2"; shift 2 ;; + --skip-docker) SKIP_DOCKER=true; shift ;; + --skip-wasm) SKIP_WASM=true; shift ;; + --help|-h) usage ;; + *) error "Unknown option: $1"; echo "Run with --help for usage."; exit 1 ;; + esac +done + +# --- Validate project root --- +cd "$PROJECT_ROOT" + +if [ ! -f "package.json" ] || [ ! -f "src/js/const/cdn-version.ts" ]; then + error "This script must be run from the BentoPDF project root." + error "Expected to find package.json and src/js/const/cdn-version.ts" + exit 1 +fi + +# --- Check prerequisites --- +check_prerequisites() { + local missing=false + + if ! command -v npm &>/dev/null; then + error "npm is required but not found. Install Node.js first." + missing=true + fi + + if [ "$SKIP_DOCKER" = false ] && ! command -v docker &>/dev/null; then + error "docker is required but not found (use --skip-docker to skip)." + missing=true + fi + + if [ "$missing" = true ]; then + exit 1 + fi +} + +# --- Read versions from source code --- +read_versions() { + PYMUPDF_VERSION=$(grep "pymupdf:" src/js/const/cdn-version.ts | grep -o "'[^']*'" | tr -d "'") + GS_VERSION=$(grep "ghostscript:" src/js/const/cdn-version.ts | grep -o "'[^']*'" | tr -d "'") + APP_VERSION=$(node -p "require('./package.json').version") + + if [ -z "$PYMUPDF_VERSION" ] || [ -z "$GS_VERSION" ]; then + error "Failed to read WASM versions from src/js/const/cdn-version.ts" + exit 1 + fi +} + +# --- Interactive mode --- +interactive_mode() { + echo "" + echo -e "${BOLD}============================================================${NC}" + echo -e "${BOLD} BentoPDF Air-Gapped Deployment Preparation${NC}" + echo -e "${BOLD} App Version: ${APP_VERSION}${NC}" + echo -e "${BOLD}============================================================${NC}" + echo "" + echo " Detected WASM versions from source:" + echo " PyMuPDF: ${PYMUPDF_VERSION}" + echo " Ghostscript: ${GS_VERSION}" + echo " CoherentPDF: latest" + echo "" + + # [1] WASM base URL (REQUIRED) + echo -e "${BOLD}[1/8] WASM Base URL ${RED}(required)${NC}" + echo " The URL where WASM files will be hosted inside the air-gapped network." + echo " The script will append /pymupdf/, /gs/, /cpdf/ to this URL." + echo "" + echo " Examples:" + echo " https://internal.example.com/wasm" + echo " http://192.168.1.100/assets/wasm" + echo " https://cdn.mycompany.local/bentopdf" + echo "" + while true; do + read -r -p " URL: " WASM_BASE_URL + if [ -z "$WASM_BASE_URL" ]; then + warn "WASM base URL is required. Please enter a URL." + elif [[ ! "$WASM_BASE_URL" =~ ^https?:// ]]; then + warn "Must start with http:// or https://. Try again." + else + break + fi + done + echo "" + + # [2] Docker image name (optional) + echo -e "${BOLD}[2/8] Docker Image Name ${GREEN}(optional)${NC}" + echo " The name used to tag the Docker image (used with 'docker run')." + read -r -p " Image name [${IMAGE_NAME}]: " input + IMAGE_NAME="${input:-$IMAGE_NAME}" + echo "" + + # [3] Simple mode (optional) + echo -e "${BOLD}[3/8] Simple Mode ${GREEN}(optional)${NC}" + echo " Hides navigation, hero, features, FAQ — shows only PDF tools." + read -r -p " Enable Simple Mode? (y/N): " input + if [[ "${input:-}" =~ ^[Yy]$ ]]; then + SIMPLE_MODE="true" + fi + echo "" + + # [4] Default language (optional) + echo -e "${BOLD}[4/8] Default UI Language ${GREEN}(optional)${NC}" + echo " Supported: en, ar, be, da, de, es, fr, id, it, nl, pt, tr, vi, zh, zh-TW" + while true; do + read -r -p " Language [en]: " input + LANGUAGE="${input:-}" + if [ -z "$LANGUAGE" ] || echo " en ar be da de es fr id it nl pt tr vi zh zh-TW " | grep -q " $LANGUAGE "; then + break + fi + warn "Invalid language code '${LANGUAGE}'. Try again." + done + echo "" + + # [5] Custom branding (optional) + echo -e "${BOLD}[5/8] Custom Branding ${GREEN}(optional)${NC}" + echo " Replace the default BentoPDF name, logo, and footer text." + read -r -p " Brand name [BentoPDF]: " input + BRAND_NAME="${input:-}" + if [ -n "$BRAND_NAME" ]; then + echo " Place your logo in the public/ folder before building." + read -r -p " Logo path relative to public/ [images/favicon-no-bg.svg]: " input + BRAND_LOGO="${input:-}" + read -r -p " Footer text [© 2026 BentoPDF. All rights reserved.]: " input + FOOTER_TEXT="${input:-}" + fi + echo "" + + # [6] Base URL (optional) + echo -e "${BOLD}[6/8] Base URL ${GREEN}(optional)${NC}" + echo " Set this if hosting under a subdirectory (e.g. /pdf/)." + read -r -p " Base URL [/]: " input + BASE_URL="${input:-}" + echo "" + + # [7] Dockerfile (optional) + echo -e "${BOLD}[7/8] Dockerfile ${GREEN}(optional)${NC}" + echo " Options: Dockerfile (standard) or Dockerfile.nonroot (custom PUID/PGID)" + read -r -p " Dockerfile [${DOCKERFILE}]: " input + DOCKERFILE="${input:-$DOCKERFILE}" + echo "" + + # [8] Output directory (optional) + echo -e "${BOLD}[8/8] Output Directory ${GREEN}(optional)${NC}" + read -r -p " Path [${OUTPUT_DIR}]: " input + OUTPUT_DIR="${input:-$OUTPUT_DIR}" + + # Confirm + echo "" + echo -e "${BOLD}--- Configuration Summary ---${NC}" + echo "" + echo " WASM Base URL: ${WASM_BASE_URL}" + echo " Image Name: ${IMAGE_NAME}" + echo " Dockerfile: ${DOCKERFILE}" + echo " Simple Mode: ${SIMPLE_MODE:-false}" + echo " Language: ${LANGUAGE:-en (default)}" + echo " Brand Name: ${BRAND_NAME:-BentoPDF (default)}" + [ -n "$BRAND_NAME" ] && echo " Brand Logo: ${BRAND_LOGO:-images/favicon-no-bg.svg (default)}" + [ -n "$BRAND_NAME" ] && echo " Footer Text: ${FOOTER_TEXT:-(default)}" + echo " Base URL: ${BASE_URL:-/ (root)}" + echo " Output: ${OUTPUT_DIR}" + echo "" + read -r -p " Proceed? (Y/n): " input + if [[ "${input:-Y}" =~ ^[Nn]$ ]]; then + echo "Aborted." + exit 0 + fi +} + +# --- SHA-256 checksum (cross-platform) --- +sha256() { + if command -v sha256sum &>/dev/null; then + sha256sum "$1" | awk '{print $1}' + elif command -v shasum &>/dev/null; then + shasum -a 256 "$1" | awk '{print $1}' + else + echo "n/a" + fi +} + +# --- File size (human-readable, cross-platform) --- +filesize() { + if stat --version &>/dev/null 2>&1; then + # GNU stat (Linux) + stat --printf='%s' "$1" 2>/dev/null | awk '{ + if ($1 >= 1073741824) printf "%.1f GB", $1/1073741824; + else if ($1 >= 1048576) printf "%.1f MB", $1/1048576; + else if ($1 >= 1024) printf "%.1f KB", $1/1024; + else printf "%d B", $1; + }' + else + # BSD stat (macOS) + stat -f '%z' "$1" 2>/dev/null | awk '{ + if ($1 >= 1073741824) printf "%.1f GB", $1/1073741824; + else if ($1 >= 1048576) printf "%.1f MB", $1/1048576; + else if ($1 >= 1024) printf "%.1f KB", $1/1024; + else printf "%d B", $1; + }' + fi +} + +# ============================================================ +# MAIN +# ============================================================ + +check_prerequisites +read_versions + +# If no WASM base URL provided, go interactive +if [ -z "$WASM_BASE_URL" ]; then + INTERACTIVE=true + interactive_mode +fi + +# Validate language code if provided +if [ -n "$LANGUAGE" ]; then + VALID_LANGS="en ar be da de es fr id it nl pt tr vi zh zh-TW" + if ! echo " $VALID_LANGS " | grep -q " $LANGUAGE "; then + error "Invalid language code: ${LANGUAGE}" + error "Supported: ${VALID_LANGS}" + exit 1 + fi +fi + +# Validate WASM base URL format +if [[ ! "$WASM_BASE_URL" =~ ^https?:// ]]; then + error "WASM base URL must start with http:// or https://" + error " Got: ${WASM_BASE_URL}" + error " Example: https://internal.example.com/wasm" + exit 1 +fi + +# Strip trailing slash from WASM base URL +WASM_BASE_URL="${WASM_BASE_URL%/}" + +# Construct WASM URLs +WASM_PYMUPDF_URL="${WASM_BASE_URL}/pymupdf/" +WASM_GS_URL="${WASM_BASE_URL}/gs/" +WASM_CPDF_URL="${WASM_BASE_URL}/cpdf/" + +echo "" +echo -e "${BOLD}============================================================${NC}" +echo -e "${BOLD} BentoPDF Air-Gapped Bundle Preparation${NC}" +echo -e "${BOLD} App: v${APP_VERSION} | PyMuPDF: ${PYMUPDF_VERSION} | GS: ${GS_VERSION}${NC}" +echo -e "${BOLD}============================================================${NC}" + +# --- Phase 1: Prepare output directory --- +step "Preparing output directory" + +mkdir -p "$OUTPUT_DIR" +OUTPUT_DIR="$(cd "$OUTPUT_DIR" && pwd)" + +# Warn if output directory already has bundle files +if ls "$OUTPUT_DIR"/*.tgz "$OUTPUT_DIR"/bentopdf.tar "$OUTPUT_DIR"/setup.sh 2>/dev/null | head -1 &>/dev/null; then + warn "Output directory already contains files from a previous run." + warn "Existing files will be overwritten." + if [ "$INTERACTIVE" = true ]; then + read -r -p " Continue? (Y/n): " input + if [[ "${input:-Y}" =~ ^[Nn]$ ]]; then + echo "Aborted." + exit 0 + fi + fi +fi + +info "Output: ${OUTPUT_DIR}" + +# --- Phase 2: Download WASM packages --- +if [ "$SKIP_WASM" = true ]; then + step "Skipping WASM download (--skip-wasm)" + # Verify each file exists with specific errors + wasm_missing=false + if ! ls "$OUTPUT_DIR"/bentopdf-pymupdf-wasm-*.tgz &>/dev/null; then + error "Missing: bentopdf-pymupdf-wasm-*.tgz" + wasm_missing=true + fi + if ! ls "$OUTPUT_DIR"/bentopdf-gs-wasm-*.tgz &>/dev/null; then + error "Missing: bentopdf-gs-wasm-*.tgz" + wasm_missing=true + fi + if ! ls "$OUTPUT_DIR"/coherentpdf-*.tgz &>/dev/null; then + error "Missing: coherentpdf-*.tgz" + wasm_missing=true + fi + if [ "$wasm_missing" = true ]; then + error "Run without --skip-wasm first to download the packages." + exit 1 + fi + success "Reusing existing WASM packages" +else + step "Downloading WASM packages" + + WASM_TMP=$(mktemp -d) + trap 'rm -rf "$WASM_TMP"' EXIT + + info "Downloading @bentopdf/pymupdf-wasm@${PYMUPDF_VERSION}..." + if ! (cd "$WASM_TMP" && npm pack "@bentopdf/pymupdf-wasm@${PYMUPDF_VERSION}" --quiet 2>&1); then + error "Failed to download @bentopdf/pymupdf-wasm@${PYMUPDF_VERSION}" + error "Check your internet connection and that the package exists on npm." + exit 1 + fi + + info "Downloading @bentopdf/gs-wasm@${GS_VERSION}..." + if ! (cd "$WASM_TMP" && npm pack "@bentopdf/gs-wasm@${GS_VERSION}" --quiet 2>&1); then + error "Failed to download @bentopdf/gs-wasm@${GS_VERSION}" + error "Check your internet connection and that the package exists on npm." + exit 1 + fi + + info "Downloading coherentpdf..." + if ! (cd "$WASM_TMP" && npm pack coherentpdf --quiet 2>&1); then + error "Failed to download coherentpdf" + error "Check your internet connection and that the package exists on npm." + exit 1 + fi + + # Move to output directory + mv "$WASM_TMP"/*.tgz "$OUTPUT_DIR/" + rm -rf "$WASM_TMP" + trap - EXIT + + # Resolve CoherentPDF version from filename + CPDF_TGZ=$(ls "$OUTPUT_DIR"/coherentpdf-*.tgz 2>/dev/null | head -1) + CPDF_VERSION=$(basename "$CPDF_TGZ" | sed 's/coherentpdf-\(.*\)\.tgz/\1/') + + success "Downloaded all WASM packages" + info " PyMuPDF: $(filesize "$OUTPUT_DIR"/bentopdf-pymupdf-wasm-*.tgz)" + info " Ghostscript: $(filesize "$OUTPUT_DIR"/bentopdf-gs-wasm-*.tgz)" + info " CoherentPDF: $(filesize "$CPDF_TGZ") (v${CPDF_VERSION})" +fi + +# Resolve CPDF version if we skipped download +if [ -z "${CPDF_VERSION:-}" ]; then + CPDF_TGZ=$(ls "$OUTPUT_DIR"/coherentpdf-*.tgz 2>/dev/null | head -1) + CPDF_VERSION=$(basename "$CPDF_TGZ" | sed 's/coherentpdf-\(.*\)\.tgz/\1/') +fi + +# --- Phase 3: Build Docker image --- +if [ "$SKIP_DOCKER" = true ]; then + step "Skipping Docker build (--skip-docker)" + + # Check if image exists or tar exists + if [ -f "$OUTPUT_DIR/bentopdf.tar" ]; then + success "Reusing existing bentopdf.tar" + elif docker image inspect "$IMAGE_NAME" &>/dev/null; then + step "Exporting existing Docker image" + docker save "$IMAGE_NAME" -o "$OUTPUT_DIR/bentopdf.tar" + success "Exported: $(filesize "$OUTPUT_DIR/bentopdf.tar")" + else + warn "No Docker image '${IMAGE_NAME}' found and no bentopdf.tar in output." + warn "The bundle will not include a Docker image." + fi +else + step "Building Docker image" + + # Verify Dockerfile exists + if [ ! -f "$DOCKERFILE" ]; then + error "Dockerfile not found: ${DOCKERFILE}" + error "Available Dockerfiles:" + ls -1 Dockerfile* 2>/dev/null | sed 's/^/ /' || echo " (none found)" + exit 1 + fi + + # Verify Docker daemon is running + if ! docker info &>/dev/null; then + error "Docker daemon is not running. Start Docker and try again." + exit 1 + fi + + # Build the docker build command + BUILD_ARGS=() + BUILD_ARGS+=(--build-arg "VITE_WASM_PYMUPDF_URL=${WASM_PYMUPDF_URL}") + BUILD_ARGS+=(--build-arg "VITE_WASM_GS_URL=${WASM_GS_URL}") + BUILD_ARGS+=(--build-arg "VITE_WASM_CPDF_URL=${WASM_CPDF_URL}") + + [ -n "$SIMPLE_MODE" ] && BUILD_ARGS+=(--build-arg "SIMPLE_MODE=${SIMPLE_MODE}") + [ -n "$BASE_URL" ] && BUILD_ARGS+=(--build-arg "BASE_URL=${BASE_URL}") + [ -n "$COMPRESSION_MODE" ] && BUILD_ARGS+=(--build-arg "COMPRESSION_MODE=${COMPRESSION_MODE}") + [ -n "$LANGUAGE" ] && BUILD_ARGS+=(--build-arg "VITE_DEFAULT_LANGUAGE=${LANGUAGE}") + [ -n "$BRAND_NAME" ] && BUILD_ARGS+=(--build-arg "VITE_BRAND_NAME=${BRAND_NAME}") + [ -n "$BRAND_LOGO" ] && BUILD_ARGS+=(--build-arg "VITE_BRAND_LOGO=${BRAND_LOGO}") + [ -n "$FOOTER_TEXT" ] && BUILD_ARGS+=(--build-arg "VITE_FOOTER_TEXT=${FOOTER_TEXT}") + + info "Image name: ${IMAGE_NAME}" + info "Dockerfile: ${DOCKERFILE}" + info "WASM URLs:" + info " PyMuPDF: ${WASM_PYMUPDF_URL}" + info " Ghostscript: ${WASM_GS_URL}" + info " CoherentPDF: ${WASM_CPDF_URL}" + echo "" + info "Building... this may take a few minutes (npm install + Vite build)." + echo "" + + docker build -f "$DOCKERFILE" "${BUILD_ARGS[@]}" -t "$IMAGE_NAME" . + + success "Docker image '${IMAGE_NAME}' built successfully" + + # --- Phase 4: Export Docker image --- + step "Exporting Docker image" + + docker save "$IMAGE_NAME" -o "$OUTPUT_DIR/bentopdf.tar" + success "Exported: $(filesize "$OUTPUT_DIR/bentopdf.tar")" +fi + +# --- Phase 5: Generate setup.sh --- +step "Generating setup script" + +cat > "$OUTPUT_DIR/setup.sh" </dev/null; then + echo "ERROR: docker is required but not found." + echo "Install Docker first: https://docs.docker.com/get-docker/" + exit 1 +fi + +if ! docker info &>/dev/null; then + echo "ERROR: Docker daemon is not running. Start Docker and try again." + exit 1 +fi + +# --- Configuration (baked in at generation time) --- +IMAGE_NAME="${IMAGE_NAME}" +WASM_BASE_URL="${WASM_BASE_URL}" +DOCKER_PORT="\${1:-3000}" + +# Where to extract WASM files (override with WASM_EXTRACT_DIR env var) +WASM_DIR="\${WASM_EXTRACT_DIR:-\${SCRIPT_DIR}/wasm}" + +echo "" +echo "============================================================" +echo " BentoPDF Air-Gapped Setup" +echo " Version: ${APP_VERSION}" +echo "============================================================" +echo "" +echo " Docker image: \${IMAGE_NAME}" +echo " WASM base URL: \${WASM_BASE_URL}" +echo " WASM extract: \${WASM_DIR}" +echo " Port: \${DOCKER_PORT}" +echo "" + +# --- Step 1: Load Docker Image --- +echo "[1/3] Loading Docker image..." +if [ -f "\${SCRIPT_DIR}/bentopdf.tar" ]; then + docker load -i "\${SCRIPT_DIR}/bentopdf.tar" + echo " Docker image '\${IMAGE_NAME}' loaded." +else + echo " WARNING: bentopdf.tar not found. Skipping Docker load." + echo " Make sure the image '\${IMAGE_NAME}' is already available." +fi + +# --- Step 2: Extract WASM Packages --- +echo "" +echo "[2/3] Extracting WASM packages to \${WASM_DIR}..." + +mkdir -p "\${WASM_DIR}/pymupdf" "\${WASM_DIR}/gs" "\${WASM_DIR}/cpdf" + +# PyMuPDF: package has dist/ and assets/ at root +echo " Extracting PyMuPDF..." +tar xzf "\${SCRIPT_DIR}"/bentopdf-pymupdf-wasm-*.tgz -C "\${WASM_DIR}/pymupdf" --strip-components=1 + +# Ghostscript: browser expects gs.js and gs.wasm at root +echo " Extracting Ghostscript..." +TEMP_GS="\$(mktemp -d)" +tar xzf "\${SCRIPT_DIR}"/bentopdf-gs-wasm-*.tgz -C "\${TEMP_GS}" +if [ -d "\${TEMP_GS}/package/assets" ]; then + cp -r "\${TEMP_GS}/package/assets/"* "\${WASM_DIR}/gs/" +else + cp -r "\${TEMP_GS}/package/"* "\${WASM_DIR}/gs/" +fi +rm -rf "\${TEMP_GS}" + +# CoherentPDF: browser expects coherentpdf.browser.min.js at root +echo " Extracting CoherentPDF..." +TEMP_CPDF="\$(mktemp -d)" +tar xzf "\${SCRIPT_DIR}"/coherentpdf-*.tgz -C "\${TEMP_CPDF}" +if [ -d "\${TEMP_CPDF}/package/dist" ]; then + cp -r "\${TEMP_CPDF}/package/dist/"* "\${WASM_DIR}/cpdf/" +else + cp -r "\${TEMP_CPDF}/package/"* "\${WASM_DIR}/cpdf/" +fi +rm -rf "\${TEMP_CPDF}" + +echo " WASM files extracted to: \${WASM_DIR}" +echo "" +echo " IMPORTANT: Ensure these paths are served by your internal web server:" +echo " \${WASM_BASE_URL}/pymupdf/ -> \${WASM_DIR}/pymupdf/" +echo " \${WASM_BASE_URL}/gs/ -> \${WASM_DIR}/gs/" +echo " \${WASM_BASE_URL}/cpdf/ -> \${WASM_DIR}/cpdf/" + +# --- Step 3: Start BentoPDF --- +echo "" +echo "[3/3] Ready to start BentoPDF" +echo "" +echo " To start manually:" +echo " docker run -d --name bentopdf -p \${DOCKER_PORT}:8080 --restart unless-stopped \${IMAGE_NAME}" +echo "" +echo " Then open: http://localhost:\${DOCKER_PORT}" +echo "" + +read -r -p "Start BentoPDF now? (y/N): " REPLY +if [[ "\${REPLY:-}" =~ ^[Yy]$ ]]; then + docker run -d --name bentopdf -p "\${DOCKER_PORT}:8080" --restart unless-stopped "\${IMAGE_NAME}" + echo "" + echo " BentoPDF is running at http://localhost:\${DOCKER_PORT}" +fi +SETUP_EOF + +chmod +x "$OUTPUT_DIR/setup.sh" +success "Generated setup.sh" + +# --- Phase 6: Generate README --- +step "Generating README" + +cat > "$OUTPUT_DIR/README.md" < @@ -460,14 +462,17 @@
Bento PDF Logo - BentoPDF + {{#if brandName}}{{brandName}}{{else}}BentoPDF{{/if}}

- © 2026 BentoPDF. All rights reserved. + {{#if footerText}}{{footerText}}{{else}}© 2026 BentoPDF. All + rights reserved.{{/if}}

Version diff --git a/src/js/main.ts b/src/js/main.ts index afffd92..d44d7e9 100644 --- a/src/js/main.ts +++ b/src/js/main.ts @@ -15,6 +15,7 @@ import { createLanguageSwitcher, t, } from './i18n/index.js'; +declare const __BRAND_NAME__: string; const init = async () => { await initI18n(); @@ -81,18 +82,19 @@ const init = async () => { (divider as HTMLElement).style.display = 'none'; }); - document.title = 'BentoPDF - PDF Tools'; + const brandName = __BRAND_NAME__ || 'BentoPDF'; + document.title = `${brandName} - ${t('simpleMode.title')}`; const toolsHeader = document.getElementById('tools-header'); if (toolsHeader) { const title = toolsHeader.querySelector('h2'); const subtitle = toolsHeader.querySelector('p'); if (title) { - title.textContent = 'PDF Tools'; + title.textContent = t('simpleMode.title'); title.className = 'text-4xl md:text-5xl font-bold text-white mb-3'; } if (subtitle) { - subtitle.textContent = 'Select a tool to get started'; + subtitle.textContent = t('simpleMode.subtitle'); subtitle.className = 'text-lg text-gray-400'; } } @@ -805,8 +807,12 @@ const init = async () => { left.className = 'flex items-center gap-3'; const icon = document.createElement('i'); - icon.className = 'w-5 h-5 text-indigo-400'; - icon.setAttribute('data-lucide', tool.icon); + if (tool.icon.startsWith('ph-')) { + icon.className = `ph ${tool.icon} w-5 h-5 text-indigo-400`; + } else { + icon.className = 'w-5 h-5 text-indigo-400'; + icon.setAttribute('data-lucide', tool.icon); + } const name = document.createElement('span'); name.className = 'text-gray-200 font-medium'; diff --git a/src/js/workflow/nodes/divide-pages-node.ts b/src/js/workflow/nodes/divide-pages-node.ts index d5d8dd1..1334697 100644 --- a/src/js/workflow/nodes/divide-pages-node.ts +++ b/src/js/workflow/nodes/divide-pages-node.ts @@ -4,6 +4,7 @@ import { pdfSocket } from '../sockets'; import type { SocketData } from '../types'; import { requirePdfInput, processBatch } from '../types'; import { PDFDocument } from 'pdf-lib'; +import { parsePageRange } from '../../utils/pdf-operations'; export class DividePagesNode extends BaseWorkflowNode { readonly category = 'Organize & Manage' as const; @@ -18,6 +19,10 @@ export class DividePagesNode extends BaseWorkflowNode { 'direction', new ClassicPreset.InputControl('text', { initial: 'vertical' }) ); + this.addControl( + 'pages', + new ClassicPreset.InputControl('text', { initial: '' }) + ); } async data( @@ -30,23 +35,39 @@ export class DividePagesNode extends BaseWorkflowNode { const direction = dirCtrl?.value === 'horizontal' ? 'horizontal' : 'vertical'; + const pagesCtrl = this.controls['pages'] as + | ClassicPreset.InputControl<'text'> + | undefined; + const rangeStr = (pagesCtrl?.value || '').trim(); + return { pdf: await processBatch(pdfInputs, async (input) => { const srcDoc = await PDFDocument.load(input.bytes); const newDoc = await PDFDocument.create(); - for (let i = 0; i < srcDoc.getPageCount(); i++) { - const [page1] = await newDoc.copyPages(srcDoc, [i]); - const [page2] = await newDoc.copyPages(srcDoc, [i]); - const { width, height } = page1.getSize(); - if (direction === 'vertical') { - page1.setCropBox(0, 0, width / 2, height); - page2.setCropBox(width / 2, 0, width / 2, height); + const totalPages = srcDoc.getPageCount(); + + const pagesToDivide: Set = rangeStr + ? new Set(parsePageRange(rangeStr, totalPages)) + : new Set(Array.from({ length: totalPages }, (_, i) => i)); + + for (let i = 0; i < totalPages; i++) { + if (pagesToDivide.has(i)) { + const [page1] = await newDoc.copyPages(srcDoc, [i]); + const [page2] = await newDoc.copyPages(srcDoc, [i]); + const { width, height } = page1.getSize(); + if (direction === 'vertical') { + page1.setCropBox(0, 0, width / 2, height); + page2.setCropBox(width / 2, 0, width / 2, height); + } else { + page1.setCropBox(0, height / 2, width, height / 2); + page2.setCropBox(0, 0, width, height / 2); + } + newDoc.addPage(page1); + newDoc.addPage(page2); } else { - page1.setCropBox(0, height / 2, width, height / 2); - page2.setCropBox(0, 0, width, height / 2); + const [copiedPage] = await newDoc.copyPages(srcDoc, [i]); + newDoc.addPage(copiedPage); } - newDoc.addPage(page1); - newDoc.addPage(page2); } const pdfBytes = await newDoc.save(); return { diff --git a/src/pages/pdf-multi-tool.html b/src/pages/pdf-multi-tool.html index 68abb18..da35359 100644 --- a/src/pages/pdf-multi-tool.html +++ b/src/pages/pdf-multi-tool.html @@ -133,12 +133,14 @@

Bento PDF Logo - BentoPDF + {{#if brandName}}{{brandName}}{{else}}BentoPDF{{/if}} PDF Multi Tool
- BentoPDF + {{#if brandName}}{{brandName}}{{else}}BentoPDF{{/if}}
-

- © 2026 BentoPDF. All rights reserved. +

Version diff --git a/src/partials/footer.html b/src/partials/footer.html index 78a88ab..1298255 100644 --- a/src/partials/footer.html +++ b/src/partials/footer.html @@ -5,14 +5,15 @@

- BentoPDF + {{#if brandName}}{{brandName}}{{else}}BentoPDF{{/if}}
-

- © 2026 BentoPDF. All rights reserved. +

Version diff --git a/src/partials/navbar-simple.html b/src/partials/navbar-simple.html index 1b1021b..a3d174c 100644 --- a/src/partials/navbar-simple.html +++ b/src/partials/navbar-simple.html @@ -6,12 +6,15 @@

diff --git a/src/partials/navbar.html b/src/partials/navbar.html index 6f97076..ae540cf 100644 --- a/src/partials/navbar.html +++ b/src/partials/navbar.html @@ -7,12 +7,15 @@ id="home-logo" > - - BentoPDF + + {{#if brandName}}{{brandName}}{{else}}BentoPDF{{/if}}
diff --git a/vite.config.ts b/vite.config.ts index 0ee2df8..45b9017 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -293,6 +293,9 @@ export default defineConfig(() => { context: { baseUrl: (process.env.BASE_URL || '/').replace(/\/?$/, '/'), simpleMode: process.env.SIMPLE_MODE === 'true', + brandName: process.env.VITE_BRAND_NAME || '', + brandLogo: process.env.VITE_BRAND_LOGO || '', + footerText: process.env.VITE_FOOTER_TEXT || '', }, }), languageRouterPlugin(), @@ -334,6 +337,7 @@ export default defineConfig(() => { ], define: { __SIMPLE_MODE__: JSON.stringify(process.env.SIMPLE_MODE === 'true'), + __BRAND_NAME__: JSON.stringify(process.env.VITE_BRAND_NAME || ''), }, resolve: { alias: {