Compare commits

..
Author SHA1 Message Date
github-actions[bot] 0a729c3681 chore: update docker downloads badge (15.1M) 2026-11-02 04:21:33 +00:00
1717 changed files with 7 additions and 621831 deletions
-10
View File
@@ -1,10 +0,0 @@
node_modules
npm-debug.log
dist
coverage
.git
.gitignore
.env
*.md
.vscode
.idea
-2
View File
@@ -1,2 +0,0 @@
# Dev-only: route TSA/certificate requests through the built-in Vite CORS proxy
VITE_CORS_PROXY_URL=/cors-proxy
-33
View File
@@ -1,33 +0,0 @@
# BentoPDF Environment Variables
# Copy this file to .env.production and configure as needed.
# CORS Proxy for digital signature certificate chain fetching
VITE_CORS_PROXY_URL=
VITE_CORS_PROXY_SECRET=
# WASM Module URLs
# Pre-configured defaults enable advanced PDF features out of the box.
# For air-gapped / offline deployments, point these to your internal server (e.g., /wasm/pymupdf/).
VITE_WASM_PYMUPDF_URL=https://cdn.jsdelivr.net/npm/@bentopdf/[email protected]/
VITE_WASM_GS_URL=https://cdn.jsdelivr.net/npm/@bentopdf/[email protected]/assets/
VITE_WASM_CPDF_URL=https://cdn.jsdelivr.net/npm/[email protected]/dist/
# OCR assets (optional)
# Set all three together for self-hosted or air-gapped OCR.
# Leave empty to use Tesseract.js runtime defaults.
VITE_TESSERACT_WORKER_URL=
VITE_TESSERACT_CORE_URL=
VITE_TESSERACT_LANG_URL=
VITE_TESSERACT_AVAILABLE_LANGUAGES=
VITE_OCR_FONT_BASE_URL=
# Default UI language (build-time)
# Supported: en, ar, be, fr, de, es, zh, zh-TW, vi, tr, id, it, pt, nl, da, ko, sv, ru
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=
-2
View File
@@ -1,2 +0,0 @@
github: alam00000
ko_fi: alio01
-122
View File
@@ -1,122 +0,0 @@
name: 🐛 Bug Report
description: Report a bug in BentoPDF
title: "(Bug) "
labels: ["bug", "needs triage"]
body:
- type: markdown
attributes:
value: |
## ⚠️ Important Notice
**Bug reports without logs or a sample file demonstrating the issue will not be investigated.**
Please help us help you by providing the information needed to reproduce and fix the problem.
- type: textarea
id: description
attributes:
label: Description
description: Provide a clear and concise description of the bug.
placeholder: What happened? What did you expect to happen?
validations:
required: true
- type: textarea
id: steps
attributes:
label: Steps to Reproduce
description: How can we reproduce this issue?
placeholder: |
1. Go to '...'
2. Click on '...'
3. Upload file '...'
4. See error
validations:
required: true
- type: textarea
id: console-logs
attributes:
label: Console Logs
description: Open browser DevTools (F12 → Console tab) and paste any errors here.
placeholder: Paste console logs here...
render: shell
validations:
required: true
- type: textarea
id: sample-file
attributes:
label: Sample PDF or File
description: |
Attach a sample PDF that reproduces the issue, or describe how to create one.
If you cannot share the original, create a minimal example that shows the problem.
placeholder: Drag and drop your file here, or describe how to reproduce with any PDF...
validations:
required: true
- type: dropdown
id: browser
attributes:
label: Browser
description: Which browser are you using?
options:
- Chrome
- Firefox
- Safari
- Edge
- Brave
- Other
validations:
required: true
- type: input
id: browser-version
attributes:
label: Browser Version
description: e.g., Chrome 120, Firefox 121
placeholder: "120"
validations:
required: true
- type: dropdown
id: os
attributes:
label: Operating System
options:
- macOS
- Windows
- Linux
- iOS
- Android
- Other
validations:
required: true
- type: input
id: bentopdf-version
attributes:
label: BentoPDF Version
description: Check the footer or package.json
placeholder: "1.15.4"
validations:
required: false
- type: textarea
id: additional
attributes:
label: Additional Context
description: Any other information that might help us debug this issue.
placeholder: Screenshots, network errors, stack traces, etc.
validations:
required: false
- type: checkboxes
id: checklist
attributes:
label: Pre-submission Checklist
options:
- label: I have included console logs from the browser DevTools
required: true
- label: I have attached a sample file or described how to reproduce the issue
required: true
- label: I have searched existing issues to ensure this is not a duplicate
required: true
-8
View File
@@ -1,8 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: 💬 Discord Community
url: https://discord.gg/Bgq3Ay3f2w
about: Join our Discord for quick questions and community support
- name: 📖 Documentation
url: https://github.com/nicholaschen09/BentoPDF#readme
about: Check the README for setup and usage instructions
@@ -1,39 +0,0 @@
name: 💡 Feature Request
description: Suggest a new feature for BentoPDF
title: "(Feature) "
labels: ["enhancement", "needs triage"]
body:
- type: textarea
id: description
attributes:
label: Feature Description
description: What functionality are you requesting?
placeholder: Describe the feature you'd like to see...
validations:
required: true
- type: textarea
id: use-case
attributes:
label: Use Case
description: Why is this feature useful? What problem does it solve?
placeholder: Explain why you need this feature...
validations:
required: true
- type: textarea
id: examples
attributes:
label: Examples
description: Any examples, mockups, or references to illustrate the feature?
placeholder: Links to similar features, screenshots, etc.
validations:
required: false
- type: textarea
id: additional
attributes:
label: Additional Context
description: Any other information about the feature request.
validations:
required: false
-30
View File
@@ -1,30 +0,0 @@
name: ❓ Question
description: Ask a question about BentoPDF
title: "(Question) "
labels: ["question"]
body:
- type: textarea
id: question
attributes:
label: Question
description: What would you like to know?
placeholder: Your question here...
validations:
required: true
- type: textarea
id: tried
attributes:
label: What have you tried?
description: What solutions have you already attempted?
placeholder: Describe what you've tried so far...
validations:
required: false
- type: textarea
id: context
attributes:
label: Additional Context
description: Any relevant code snippets, screenshots, or scenarios.
validations:
required: false
-18
View File
@@ -1,18 +0,0 @@
{
"name": "BentoPDF CLA",
"text": [
"Thank you for your contribution to BentoPDF!",
"",
"By signing this CLA, you agree to the terms of our Contributor License Agreement.",
"",
"**Individual Contributors**: Please review and sign our [Individual CLA (ICLA)](ICLA.md)",
"",
"**Corporate Contributors**: If you are contributing on behalf of your employer, please have your organization sign our [Corporate CLA (CCLA)](CCLA.md) by contacting [email protected]"
],
"notes": {
"purpose": "This CLA enables BentoPDF's dual licensing model (AGPL-3.0 + commercial license)",
"icla_url": "https://github.com/alam00000/bentopdf/blob/main/ICLA.md",
"ccla_url": "https://github.com/alam00000/bentopdf/blob/main/CCLA.md",
"contact": "[email protected]"
}
}
-28
View File
@@ -1,28 +0,0 @@
name: BentoPDF CodeQL config
paths-ignore:
- dist
- dist-test
- coverage
- node_modules
- vendor
- bentopdf-pymupdf-wasm
- libreoffice-wasm-package
- bentopdf-airgap-bundle
- public/pdfjs-viewer
- public/pdfjs-annotation-viewer
- public/libreoffice-wasm
- public/coherentpdf.browser.min.js
- public/workers
- public/embedpdf
- docs/.vitepress
- '**/*.min.js'
- '**/*.d.ts'
query-filters:
- exclude:
id: js/log-injection
- exclude:
id: js/tainted-format-string
- exclude:
id: js/file-system-race
-20
View File
@@ -1,20 +0,0 @@
version: 2
updates:
- package-ecosystem: 'npm'
directory: '/'
schedule:
interval: 'daily'
groups:
npm-dependencies:
patterns:
- '*'
- package-ecosystem: 'github-actions'
directory: '/'
schedule:
interval: 'daily'
- package-ecosystem: 'docker'
directory: '/'
schedule:
interval: 'weekly'
-44
View File
@@ -1,44 +0,0 @@
### Description
Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change.
Fixes # (issue)
### Type of change
Please delete options that are not relevant.
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] This change requires a documentation update
### 🧪 How Has This Been Tested?
Please describe **how you tested your changes** so that maintainers can verify them. Include enough detail for someone else to reproduce your tests.
**Checklist:**
- [ ] Verified output manually
- [ ] Tested with relevant sample documents or data
- [ ] Wrote Vite Test Case (if applicable)
**Expected Results:**
- What the tests should produce (e.g., correct PDF rendering, no errors).
**Actual Results:**
- What you observed when running the tests.
### Checklist:
- [ ] I have signed the [Contributor License Agreement (CLA)](ICLA.md) or my organization has signed the [Corporate CLA](CCLA.md)
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my own code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream modules
-329
View File
@@ -1,329 +0,0 @@
name: Build and Push Docker Images (Default + Simple Mode)
on:
push:
branches:
- "main"
tags:
- "v*"
workflow_dispatch:
jobs:
# New job to build dist and create release
build-and-release:
runs-on: ubuntu-latest
if: github.repository == 'alam00000/bentopdf' && startsWith(github.ref, 'refs/tags/')
permissions:
contents: write
env:
HUSKY: 0
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm run test:run
- name: Build distribution
run: npm run build
- name: Extract version
id: version
run: |
VERSION=${GITHUB_REF#refs/tags/v}
echo "version=${VERSION}" >> $GITHUB_OUTPUT
- name: Package distribution
run: npm run package
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
files: |
dist-${{ steps.version.outputs.version }}.zip
dist-simple-${{ steps.version.outputs.version }}.zip
generate_release_notes: true
draft: false
prerelease: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Build each platform natively in parallel, then merge manifests
build-amd64:
runs-on: ubuntu-latest
if: github.repository == 'alam00000/bentopdf'
permissions:
contents: read
packages: write
strategy:
matrix:
mode:
- name: default
simple_mode: false
suffix: ""
- name: simple
simple_mode: true
suffix: "-simple"
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract version
id: version
run: |
if [[ $GITHUB_REF == refs/tags/v* ]]; then
VERSION=${GITHUB_REF#refs/tags/}
echo "version=${VERSION}" >> $GITHUB_OUTPUT
echo "version_without_v=${VERSION#v}" >> $GITHUB_OUTPUT
echo "is_release=true" >> $GITHUB_OUTPUT
else
SHORT_SHA=${GITHUB_SHA::7}
echo "version=edge" >> $GITHUB_OUTPUT
echo "short_sha=${SHORT_SHA}" >> $GITHUB_OUTPUT
echo "is_release=false" >> $GITHUB_OUTPUT
fi
- name: Build and push amd64 ${{ matrix.mode.name }} (release)
if: steps.version.outputs.is_release == 'true'
uses: docker/build-push-action@v6
with:
push: true
build-args: |
SIMPLE_MODE=${{ matrix.mode.simple_mode }}
tags: |
ghcr.io/${{ github.repository_owner }}/bentopdf${{ matrix.mode.suffix }}:${{ steps.version.outputs.version }}-amd64
platforms: linux/amd64
cache-from: type=gha,scope=amd64-${{ matrix.mode.name }}
cache-to: type=gha,mode=max,scope=amd64-${{ matrix.mode.name }}
- name: Build and push amd64 ${{ matrix.mode.name }} (edge)
if: steps.version.outputs.is_release == 'false'
uses: docker/build-push-action@v6
with:
push: true
build-args: |
SIMPLE_MODE=${{ matrix.mode.simple_mode }}
tags: |
ghcr.io/${{ github.repository_owner }}/bentopdf${{ matrix.mode.suffix }}:edge-amd64
platforms: linux/amd64
cache-from: type=gha,scope=amd64-${{ matrix.mode.name }}
cache-to: type=gha,mode=max,scope=amd64-${{ matrix.mode.name }}
build-arm64:
runs-on: ubuntu-24.04-arm # Native ARM64 runner
permissions:
contents: read
packages: write
strategy:
matrix:
mode:
- name: default
simple_mode: false
suffix: ""
- name: simple
simple_mode: true
suffix: "-simple"
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract version
id: version
run: |
if [[ $GITHUB_REF == refs/tags/v* ]]; then
VERSION=${GITHUB_REF#refs/tags/}
echo "version=${VERSION}" >> $GITHUB_OUTPUT
echo "version_without_v=${VERSION#v}" >> $GITHUB_OUTPUT
echo "is_release=true" >> $GITHUB_OUTPUT
else
SHORT_SHA=${GITHUB_SHA::7}
echo "version=edge" >> $GITHUB_OUTPUT
echo "short_sha=${SHORT_SHA}" >> $GITHUB_OUTPUT
echo "is_release=false" >> $GITHUB_OUTPUT
fi
- name: Build and push arm64 ${{ matrix.mode.name }} (release)
if: steps.version.outputs.is_release == 'true'
uses: docker/build-push-action@v6
with:
push: true
build-args: |
SIMPLE_MODE=${{ matrix.mode.simple_mode }}
tags: |
ghcr.io/${{ github.repository_owner }}/bentopdf${{ matrix.mode.suffix }}:${{ steps.version.outputs.version }}-arm64
platforms: linux/arm64
cache-from: type=gha,scope=arm64-${{ matrix.mode.name }}
cache-to: type=gha,mode=max,scope=arm64-${{ matrix.mode.name }}
- name: Build and push arm64 ${{ matrix.mode.name }} (edge)
if: steps.version.outputs.is_release == 'false'
uses: docker/build-push-action@v6
with:
push: true
build-args: |
SIMPLE_MODE=${{ matrix.mode.simple_mode }}
tags: |
ghcr.io/${{ github.repository_owner }}/bentopdf${{ matrix.mode.suffix }}:edge-arm64
platforms: linux/arm64
cache-from: type=gha,scope=arm64-${{ matrix.mode.name }}
cache-to: type=gha,mode=max,scope=arm64-${{ matrix.mode.name }}
# Merge GHCR manifests after both platforms are built
merge-manifests-ghcr:
runs-on: ubuntu-latest
needs: [build-amd64, build-arm64]
permissions:
packages: write
strategy:
matrix:
mode:
- name: default
suffix: ""
- name: simple
suffix: "-simple"
steps:
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract version
id: version
run: |
if [[ $GITHUB_REF == refs/tags/v* ]]; then
VERSION=${GITHUB_REF#refs/tags/}
echo "version=${VERSION}" >> $GITHUB_OUTPUT
echo "version_without_v=${VERSION#v}" >> $GITHUB_OUTPUT
echo "is_release=true" >> $GITHUB_OUTPUT
else
SHORT_SHA=${GITHUB_SHA::7}
echo "version=edge" >> $GITHUB_OUTPUT
echo "short_sha=${SHORT_SHA}" >> $GITHUB_OUTPUT
echo "is_release=false" >> $GITHUB_OUTPUT
fi
- name: Create and push GHCR manifest (release)
if: steps.version.outputs.is_release == 'true'
run: |
docker buildx imagetools create -t ghcr.io/${{ github.repository_owner }}/bentopdf${{ matrix.mode.suffix }}:latest \
ghcr.io/${{ github.repository_owner }}/bentopdf${{ matrix.mode.suffix }}:${{ steps.version.outputs.version }}-amd64 \
ghcr.io/${{ github.repository_owner }}/bentopdf${{ matrix.mode.suffix }}:${{ steps.version.outputs.version }}-arm64
docker buildx imagetools create -t ghcr.io/${{ github.repository_owner }}/bentopdf${{ matrix.mode.suffix }}:${{ steps.version.outputs.version }} \
ghcr.io/${{ github.repository_owner }}/bentopdf${{ matrix.mode.suffix }}:${{ steps.version.outputs.version }}-amd64 \
ghcr.io/${{ github.repository_owner }}/bentopdf${{ matrix.mode.suffix }}:${{ steps.version.outputs.version }}-arm64
docker buildx imagetools create -t ghcr.io/${{ github.repository_owner }}/bentopdf${{ matrix.mode.suffix }}:${{ steps.version.outputs.version_without_v }} \
ghcr.io/${{ github.repository_owner }}/bentopdf${{ matrix.mode.suffix }}:${{ steps.version.outputs.version }}-amd64 \
ghcr.io/${{ github.repository_owner }}/bentopdf${{ matrix.mode.suffix }}:${{ steps.version.outputs.version }}-arm64
- name: Create and push GHCR manifest (edge)
if: steps.version.outputs.is_release == 'false'
run: |
docker buildx imagetools create -t ghcr.io/${{ github.repository_owner }}/bentopdf${{ matrix.mode.suffix }}:edge \
ghcr.io/${{ github.repository_owner }}/bentopdf${{ matrix.mode.suffix }}:edge-amd64 \
ghcr.io/${{ github.repository_owner }}/bentopdf${{ matrix.mode.suffix }}:edge-arm64
docker buildx imagetools create -t ghcr.io/${{ github.repository_owner }}/bentopdf${{ matrix.mode.suffix }}:sha-${{ steps.version.outputs.short_sha }} \
ghcr.io/${{ github.repository_owner }}/bentopdf${{ matrix.mode.suffix }}:edge-amd64 \
ghcr.io/${{ github.repository_owner }}/bentopdf${{ matrix.mode.suffix }}:edge-arm64
# Copy images from GHCR to DockerHub
push-to-dockerhub:
runs-on: ubuntu-latest
needs: [merge-manifests-ghcr]
continue-on-error: true
permissions:
contents: read
packages: read
strategy:
matrix:
mode:
- name: default
suffix: ""
- name: simple
suffix: "-simple"
steps:
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
- name: Extract version
id: version
run: |
if [[ $GITHUB_REF == refs/tags/v* ]]; then
VERSION=${GITHUB_REF#refs/tags/}
echo "version=${VERSION}" >> $GITHUB_OUTPUT
echo "version_without_v=${VERSION#v}" >> $GITHUB_OUTPUT
echo "is_release=true" >> $GITHUB_OUTPUT
else
SHORT_SHA=${GITHUB_SHA::7}
echo "version=edge" >> $GITHUB_OUTPUT
echo "short_sha=${SHORT_SHA}" >> $GITHUB_OUTPUT
echo "is_release=false" >> $GITHUB_OUTPUT
fi
- name: Copy images to DockerHub (release)
if: steps.version.outputs.is_release == 'true'
run: |
docker buildx imagetools create -t bentopdfteam/bentopdf${{ matrix.mode.suffix }}:latest \
ghcr.io/${{ github.repository_owner }}/bentopdf${{ matrix.mode.suffix }}:${{ steps.version.outputs.version }}-amd64 \
ghcr.io/${{ github.repository_owner }}/bentopdf${{ matrix.mode.suffix }}:${{ steps.version.outputs.version }}-arm64
docker buildx imagetools create -t bentopdfteam/bentopdf${{ matrix.mode.suffix }}:${{ steps.version.outputs.version }} \
ghcr.io/${{ github.repository_owner }}/bentopdf${{ matrix.mode.suffix }}:${{ steps.version.outputs.version }}-amd64 \
ghcr.io/${{ github.repository_owner }}/bentopdf${{ matrix.mode.suffix }}:${{ steps.version.outputs.version }}-arm64
docker buildx imagetools create -t bentopdfteam/bentopdf${{ matrix.mode.suffix }}:${{ steps.version.outputs.version_without_v }} \
ghcr.io/${{ github.repository_owner }}/bentopdf${{ matrix.mode.suffix }}:${{ steps.version.outputs.version }}-amd64 \
ghcr.io/${{ github.repository_owner }}/bentopdf${{ matrix.mode.suffix }}:${{ steps.version.outputs.version }}-arm64
- name: Copy images to DockerHub (edge)
if: steps.version.outputs.is_release == 'false'
run: |
docker buildx imagetools create -t bentopdfteam/bentopdf${{ matrix.mode.suffix }}:edge \
ghcr.io/${{ github.repository_owner }}/bentopdf${{ matrix.mode.suffix }}:edge-amd64 \
ghcr.io/${{ github.repository_owner }}/bentopdf${{ matrix.mode.suffix }}:edge-arm64
docker buildx imagetools create -t bentopdfteam/bentopdf${{ matrix.mode.suffix }}:sha-${{ steps.version.outputs.short_sha }} \
ghcr.io/${{ github.repository_owner }}/bentopdf${{ matrix.mode.suffix }}:edge-amd64 \
ghcr.io/${{ github.repository_owner }}/bentopdf${{ matrix.mode.suffix }}:edge-arm64
-76
View File
@@ -1,76 +0,0 @@
name: "CLA Assistant"
on:
issue_comment:
types: [created]
pull_request_target:
types: [opened, closed, synchronize]
# Permissions needed for the workflow
permissions:
actions: write
contents: write
pull-requests: write
jobs:
cla-check:
runs-on: ubuntu-latest
if: >
github.event_name == 'pull_request_target' ||
(
github.event_name == 'issue_comment' &&
github.event.issue.pull_request &&
(
github.event.comment.body == 'recheck' ||
github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA'
)
)
steps:
- name: "CLA Assistant"
uses: contributor-assistant/github-action@ca4a40a7d1004f18d9960b404b97e5f30a505a08 # v2.6.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
path-to-signatures: 'signatures/cla.json'
path-to-document: 'https://github.com/${{ github.repository }}/blob/main/ICLA.md'
# Branch to store signatures
branch: 'main'
# Allowlist for bot accounts (won't need to sign CLA)
allowlist: 'bot*,*[bot],dependabot*,renovate*,github-actions*'
# Custom messages
custom-notsigned-prcomment: |
## CLA Signature Required
Thank you for your contribution! Before we can accept your pull request, you need to sign our [Contributor License Agreement (CLA)](https://github.com/${{ github.repository }}/blob/main/ICLA.md).
### Why do we need a CLA?
BentoPDF uses a dual licensing model (AGPL-3.0 for open source + commercial license). The CLA allows us to:
- Include your contributions in both open source and commercial versions
- Protect you and us legally
- Keep the project sustainable
### How to sign
To sign the CLA, please comment on this PR with:
```
I have read the CLA Document and I hereby sign the CLA
```
**For corporate contributions**, please have your organization sign our [Corporate CLA (CCLA)](https://github.com/${{ github.repository }}/blob/main/CCLA.md) by contacting us at [email protected].
---
⚠️ **Note**: You only need to sign the CLA once. Future contributions to this repository will not require re-signing.
custom-pr-sign-comment: 'I have read the CLA Document and I hereby sign the CLA'
# Lock PR while waiting for CLA signature
lock-pullrequest-aftermerge: false
# Use issues to store signatures (alternative to branch storage)
use-dco-flag: false
-41
View File
@@ -1,41 +0,0 @@
name: CodeQL
on:
push:
branches: [main]
pull_request:
branches: [main]
schedule:
- cron: '0 6 * * 1'
permissions:
contents: read
jobs:
analyze:
name: Analyze (${{ matrix.language }})
runs-on: ubuntu-latest
permissions:
security-events: write
actions: read
contents: read
strategy:
fail-fast: false
matrix:
language: [javascript-typescript]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
queries: security-extended
config-file: ./.github/codeql-config.yml
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: /language:${{ matrix.language }}
@@ -1,59 +0,0 @@
name: Update Docker Downloads Badge
on:
schedule:
- cron: '17 4 * * 1'
workflow_dispatch:
permissions:
contents: write
concurrency:
group: docker-downloads-badge
cancel-in-progress: false
jobs:
update:
if: github.repository == 'alam00000/bentopdf'
runs-on: ubuntu-latest
steps:
- name: Sum GHCR download counts
id: counts
run: |
set -euo pipefail
total=0
for pkg in bentopdf bentopdf-simple; do
raw=$(curl -sf --retry 3 --retry-delay 5 --max-time 30 \
"https://ghcr-badge.elias.eu.org/api/alam00000/bentopdf/$pkg" \
| jq -e -r '.downloadCountRaw')
case "$raw" in ''|*[!0-9]*) echo "::error::invalid count for $pkg: $raw"; exit 1 ;; esac
echo "$pkg: $raw"
total=$((total + raw))
done
if [ "$total" -le 0 ]; then echo "::error::total is zero"; exit 1; fi
message=$(awk -v n="$total" 'BEGIN{
if (n >= 1000000) printf "%.1fM", n/1000000;
else if (n >= 1000) printf "%.1fk", n/1000;
else printf "%d", n
}')
echo "total=$total" >> "$GITHUB_OUTPUT"
echo "message=$message" >> "$GITHUB_OUTPUT"
echo "combined: $total -> $message"
- name: Publish badge JSON to badges branch
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
MESSAGE: ${{ steps.counts.outputs.message }}
run: |
set -euo pipefail
mkdir badges && cd badges
jq -n --arg message "$MESSAGE" \
'{schemaVersion: 1, label: "Docker Downloads", message: $message, color: "blue", namedLogo: "docker"}' \
> docker-downloads.json
cat docker-downloads.json
git init -q -b badges
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add docker-downloads.json
git commit -qm "chore: update docker downloads badge ($MESSAGE)"
git push -qf "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" badges
-35
View File
@@ -1,35 +0,0 @@
name: SEO audit
on:
pull_request:
paths:
- '**/*.html'
- 'public/locales/**'
- 'scripts/generate-i18n-pages.mjs'
- 'scripts/generate-sitemap.mjs'
- 'scripts/seo-audit.mjs'
- 'src/partials/**'
- 'package.json'
- 'package-lock.json'
- 'vite.config.ts'
jobs:
audit:
runs-on: ubuntu-latest
env:
HUSKY: 0
SITE_URL: https://www.bentopdf.com
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: npm
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
- name: SEO audit
run: npm run seo-audit
-12
View File
@@ -1,12 +0,0 @@
name: Update Sponsors
on:
workflow_dispatch:
jobs:
update:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: JamesIves/github-sponsors-readme-action@v1
with:
token: ${{ secrets.SPONSORS_TOKEN }}
file: 'README.md'
-67
View File
@@ -1,67 +0,0 @@
# Simple workflow for deploying static content to GitHub Pages
name: Deploy static content to Pages
on:
# Runs on pushes targeting the default branch
push:
branches: ["main"]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
group: "pages"
cancel-in-progress: false
# Configure these variables under your repo settings.
# You will likely need to set BASE_URL to be the name of your repo eg 'bentopdf'
env:
SIMPLE_MODE: ${{ vars.SIMPLE_MODE }}
BASE_URL: ${{ vars.BASE_URL }}/
SITE_URL: ${{ vars.SITE_URL }}
HUSKY: 0
jobs:
# Single deploy job since we're just deploying
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Install dependencies
run: npm ci
- name: Build distribution
run: |
# export SIMPLE_MODE=${{ vars.SIMPLE_MODE }}
# export BASE_URL=${{ vars.BASE_URL }}/
npm run build
- name: Setup Pages
uses: actions/configure-pages@v5
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
# Upload entire repository
path: dist
name: github-pages-deployment
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
with:
artifact_name: github-pages-deployment
-36
View File
@@ -1,36 +0,0 @@
name: Tests
on:
push:
branches:
- 'main'
pull_request:
workflow_dispatch:
concurrency:
group: tests-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
env:
HUSKY: 0
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Typecheck
run: npx tsc --noEmit
- name: Run tests
run: npm run test:run
-102
View File
@@ -1,102 +0,0 @@
name: Trivy Security Scan
on:
push:
branches: [main]
pull_request:
branches: [main]
schedule:
- cron: '0 6 * * 1'
workflow_dispatch:
permissions:
contents: read
security-events: write
jobs:
scan-image:
runs-on: ubuntu-latest
strategy:
matrix:
image:
- name: bentopdf
file: Dockerfile
- name: bentopdf-nonroot
file: Dockerfile.nonroot
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build image for scanning
uses: docker/build-push-action@v6
with:
context: .
file: ${{ matrix.image.file }}
push: false
load: true
tags: ${{ matrix.image.name }}:scan
no-cache: true
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
image-ref: ${{ matrix.image.name }}:scan
format: sarif
output: trivy-${{ matrix.image.name }}.sarif
severity: CRITICAL,HIGH
exit-code: '1'
- name: Upload Trivy results to GitHub Security
uses: github/codeql-action/upload-sarif@v4
if: always() && hashFiles('trivy-*.sarif') != ''
with:
sarif_file: trivy-${{ matrix.image.name }}.sarif
category: trivy-${{ matrix.image.name }}
scan-dependencies:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Scan npm dependencies with Trivy
uses: aquasecurity/trivy-action@master
with:
scan-type: fs
scan-ref: .
format: sarif
output: trivy-deps.sarif
severity: CRITICAL,HIGH
scanners: vuln
- name: Upload dependency scan results
uses: github/codeql-action/upload-sarif@v4
if: always() && hashFiles('trivy-deps.sarif') != ''
with:
sarif_file: trivy-deps.sarif
category: trivy-dependencies
scan-config:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Scan Dockerfiles for misconfigurations
uses: aquasecurity/trivy-action@master
with:
scan-type: config
scan-ref: .
format: sarif
output: trivy-config.sarif
severity: CRITICAL,HIGH
- name: Upload config scan results
uses: github/codeql-action/upload-sarif@v4
if: always() && hashFiles('trivy-config.sarif') != ''
with:
sarif_file: trivy-config.sarif
category: trivy-config
@@ -1,270 +0,0 @@
name: Update PDF Engine and Viewer
on:
workflow_dispatch:
schedule:
- cron: '17 4 * * *'
permissions:
contents: read
concurrency:
group: update-engine
cancel-in-progress: false
env:
FORK_URL: https://github.com/alam00000/bentopdf-pdfium-viewer
HUSKY: 0
jobs:
build:
runs-on: ubuntu-latest
if: github.repository == 'alam00000/bentopdf'
permissions:
contents: read
outputs:
run: ${{ steps.gate.outputs.run }}
version: ${{ steps.gate.outputs.version }}
steps:
- name: Checkout repo
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Read current upstream marker
id: current
run: |
if [ -f vendor/bentopdf-viewer/.upstream-version ]; then
echo "version=$(cat vendor/bentopdf-viewer/.upstream-version)" >> "$GITHUB_OUTPUT"
else
echo "version=" >> "$GITHUB_OUTPUT"
fi
- name: Read fork HEAD
id: upstream
run: echo "version=$(git ls-remote "$FORK_URL" refs/heads/main | cut -c1-12)" >> "$GITHUB_OUTPUT"
- name: Should update?
id: gate
env:
CURRENT: ${{ steps.current.outputs.version }}
UPSTREAM: ${{ steps.upstream.outputs.version }}
run: |
if ! [[ "$UPSTREAM" =~ ^[0-9a-f]{12}$ ]]; then
echo "::error::Unexpected fork HEAD: $UPSTREAM"
exit 1
fi
echo "version=$UPSTREAM" >> "$GITHUB_OUTPUT"
if [ "$UPSTREAM" = "$CURRENT" ]; then
echo "run=false" >> "$GITHUB_OUTPUT"
echo "Already on $CURRENT."
else
echo "run=true" >> "$GITHUB_OUTPUT"
echo "Updating $CURRENT -> $UPSTREAM"
fi
- name: Setup Node
if: steps.gate.outputs.run == 'true'
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- name: Enable corepack (pnpm)
if: steps.gate.outputs.run == 'true'
run: corepack enable
- name: Clone the viewer fork
if: steps.gate.outputs.run == 'true'
run: git clone --depth 1 --branch main "$FORK_URL" ./_fork
- name: Require a published engine
if: steps.gate.outputs.run == 'true'
run: |
test -s ./_fork/packages/pdfium/src/vendor/pdfium.wasm
test -s ./_fork/packages/pdfium/src/vendor/pdfium.js
- name: Pack the engine
if: steps.gate.outputs.run == 'true'
id: engine
env:
SHA: ${{ steps.gate.outputs.version }}
run: |
set -euo pipefail
VENDOR=./_fork/packages/pdfium/src/vendor
STAGE=$(mktemp -d)/package
mkdir -p "$STAGE"
cp "$VENDOR/pdfium.js" "$STAGE/editcore.js"
cp "$VENDOR/pdfium.wasm" "$STAGE/editcore.wasm"
cat > "$STAGE/package.json" <<JSON
{
"name": "bentopdf-pdfium",
"version": "0.0.0-$SHA",
"type": "module",
"main": "editcore.js",
"exports": { ".": "./editcore.js", "./editcore.wasm": "./editcore.wasm" },
"license": "AGPL-3.0"
}
JSON
mkdir -p vendor/bentopdf-pdfium
OUT="vendor/bentopdf-pdfium/bentopdf-pdfium-$SHA.tgz"
tar -czf "$OUT" -C "$(dirname "$STAGE")" package
find vendor/bentopdf-pdfium -maxdepth 1 -name 'bentopdf-pdfium*.tgz' ! -name "$(basename "$OUT")" -delete
echo "tarball=$OUT" >> "$GITHUB_OUTPUT"
- name: Build the viewer
if: steps.gate.outputs.run == 'true'
working-directory: ./_fork
run: |
pnpm install --no-frozen-lockfile
pnpm run build:snippet
- name: Pack the viewer without a second engine
if: steps.gate.outputs.run == 'true'
id: viewer
env:
SHA: ${{ steps.gate.outputs.version }}
run: |
set -euo pipefail
WORK=$(mktemp -d)
(cd ./_fork && npm pack ./viewers/snippet --pack-destination "$WORK" >/dev/null)
TMP=$(mktemp -d)
tar -xzf "$WORK"/*.tgz -C "$TMP"
rm -f "$TMP/package/dist/pdfium.wasm"
PKG="$TMP/package/package.json" node - <<'NODE'
const fs = require('fs');
const p = process.env.PKG;
const pkg = JSON.parse(fs.readFileSync(p, 'utf8'));
pkg.name = 'bentopdf-viewer';
pkg.dependencies = pkg.dependencies || {};
// The rollup bundle inlines every @embedpdf/* package, so these
// declared deps would only pull unused copies from upstream npm.
for (const k of Object.keys(pkg.dependencies)) {
if (k.startsWith('@embedpdf/')) delete pkg.dependencies[k];
if (k === 'preact') pkg.dependencies[k] = '^10.17.0';
}
fs.writeFileSync(p, JSON.stringify(pkg, null, 2) + '\n');
NODE
mkdir -p vendor/bentopdf-viewer
OUT="vendor/bentopdf-viewer/bentopdf-viewer-$SHA.tgz"
tar -czf "$OUT" -C "$TMP" package
find vendor/bentopdf-viewer -maxdepth 1 -name 'bentopdf-viewer-*.tgz' ! -name "$(basename "$OUT")" -delete
echo "tarball=$OUT" >> "$GITHUB_OUTPUT"
- name: Point dependencies at the new tarballs
if: steps.gate.outputs.run == 'true'
env:
ENGINE_TARBALL: ${{ steps.engine.outputs.tarball }}
VIEWER_TARBALL: ${{ steps.viewer.outputs.tarball }}
run: |
node -e "
const fs = require('fs');
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
pkg.dependencies['bentopdf-pdfium'] = 'file:' + process.env.ENGINE_TARBALL;
pkg.dependencies['bentopdf-viewer'] = 'file:' + process.env.VIEWER_TARBALL;
fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n');
"
- name: Cleanup upstream clone
if: steps.gate.outputs.run == 'true'
run: rm -rf ./_fork
- name: Install and verify one shared engine
if: steps.gate.outputs.run == 'true'
run: |
set -euo pipefail
npm install
test -s node_modules/bentopdf-pdfium/editcore.wasm
if find node_modules/bentopdf-viewer -name '*.wasm' | grep -q .; then
echo "::error::viewer package still ships a wasm; the engine would be duplicated"
exit 1
fi
if ! grep -q "bentopdf-pdfium/editcore.wasm" src/js/logic/edit-pdf-page.ts; then
echo "::error::the PDF editor no longer points at the shared engine"
exit 1
fi
echo "engine sha256: $(sha256sum node_modules/bentopdf-pdfium/editcore.wasm | cut -c1-16)"
- name: Typecheck and test
if: steps.gate.outputs.run == 'true'
run: |
npx tsc --noEmit
npm run test:run
- name: Write upstream marker
if: steps.gate.outputs.run == 'true'
env:
SHA: ${{ steps.gate.outputs.version }}
run: |
echo "$SHA" > vendor/bentopdf-viewer/.upstream-version
echo "$SHA" > vendor/bentopdf-pdfium/.upstream-version
- name: Upload update
if: steps.gate.outputs.run == 'true'
uses: actions/upload-artifact@v4
with:
name: engine-update
include-hidden-files: true
retention-days: 1
path: |
package.json
package-lock.json
vendor/bentopdf-pdfium/
vendor/bentopdf-viewer/
pull-request:
runs-on: ubuntu-latest
needs: build
if: needs.build.outputs.run == 'true'
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout repo
uses: actions/checkout@v4
- name: Download update
uses: actions/download-artifact@v4
with:
name: engine-update
path: ${{ runner.temp }}/engine-update
- name: Apply update
id: apply
env:
UPDATE: ${{ runner.temp }}/engine-update
run: |
set -euo pipefail
rm -rf vendor/bentopdf-pdfium vendor/bentopdf-viewer
mkdir -p vendor
cp -R "$UPDATE/vendor/bentopdf-pdfium" "$UPDATE/vendor/bentopdf-viewer" vendor/
cp "$UPDATE/package.json" "$UPDATE/package-lock.json" .
{
echo 'checksums<<EOF'
sha256sum vendor/bentopdf-pdfium/*.tgz vendor/bentopdf-viewer/*.tgz
echo 'EOF'
} >> "$GITHUB_OUTPUT"
- name: Create Pull Request
uses: peter-evans/create-pull-request@c5a7806660adbe173f04e3e038b0ccdcd758773c # v6.1.0
with:
commit-message: 'build(deps): bump PDF engine and viewer to ${{ needs.build.outputs.version }}'
title: 'build(deps): bump PDF engine and viewer to ${{ needs.build.outputs.version }}'
body: |
Both vendored packages built from one clone of the viewer fork at
`${{ needs.build.outputs.version }}`, so the text editor and the
PDF editor run the same engine.
- `bentopdf-pdfium` — the engine wasm, packed from the fork's published `src/vendor`
- `bentopdf-viewer` — the snippet bundle, packed **without** `dist/pdfium.wasm`
The viewer loads the shared engine through `EmbedPDF.init({ wasmUrl })`.
CI verified a single wasm on disk, typecheck, and the test suite.
Built in a read-only job. Tarball checksums:
```
${{ steps.apply.outputs.checksums }}
```
branch: chore/update-pdf-engine
delete-branch: true
-56
View File
@@ -1,56 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
.npm-cache
.env
.env.production
.env.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Coverage reports
coverage/
*.lcov
# Generated sitemap
public/sitemap.xml
# Generated by scripts/generate-security-headers.mjs at build time
security-headers.conf
# CodeQL local scan artifacts (npm run security:codeql)
codeql-db/
codeql-results.sarif
codeql-results.csv
#backup
.seo-backup
libreoffice-wasm-package
# helm chart
/bentopdf-*.tgz
# test
dist-test
test
# Sibling checkouts of the engine/viewer forks
bentopdf-pdfium-viewer/
bentopdf-pymupdf-wasm/
-145
View File
@@ -1,145 +0,0 @@
RewriteEngine On
RewriteBase /
# ============================================
# 1. SECURITY HEADERS (UPDATED FOR CDN COMPATIBILITY)
# ============================================
<IfModule mod_headers.c>
Header always set X-Frame-Options "SAMEORIGIN"
Header always set X-Content-Type-Options "nosniff"
Header always set X-XSS-Protection "1; mode=block"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "geolocation=(), microphone=(), camera=()"
Header always set Cross-Origin-Opener-Policy "same-origin"
Header always set Cross-Origin-Embedder-Policy "require-corp"
Header always set Content-Security-Policy "default-src 'self'; script-src 'self' 'wasm-unsafe-eval' 'unsafe-eval' blob: https://cdn.jsdelivr.net; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: blob: https:; font-src 'self' data: https://fonts.gstatic.com https://rawcdn.githack.com; connect-src 'self' blob: https://api.github.com https://fonts.gstatic.com https://cdn.jsdelivr.net https://bentopdf-cors-proxy.bentopdf.workers.dev https://rawcdn.githack.com; object-src 'none'; base-uri 'self'; frame-src 'self' blob:; frame-ancestors 'self'; form-action 'self'"
</IfModule>
# Block source maps from being served
<FilesMatch "\.map$">
Require all denied
</FilesMatch>
# ============================================
# 2. BROWSER CACHING
# ============================================
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType image/jpeg "access plus 1 year"
ExpiresByType image/png "access plus 1 year"
ExpiresByType image/gif "access plus 1 year"
ExpiresByType image/webp "access plus 1 year"
ExpiresByType image/svg+xml "access plus 1 year"
ExpiresByType image/x-icon "access plus 1 year"
ExpiresByType font/woff2 "access plus 1 year"
ExpiresByType font/woff "access plus 1 year"
ExpiresByType font/ttf "access plus 1 year"
ExpiresByType text/css "access plus 1 month"
ExpiresByType application/javascript "access plus 1 month"
ExpiresByType application/wasm "access plus 1 year"
ExpiresByType application/gzip "access plus 1 year"
ExpiresByType text/html "access plus 0 seconds"
</IfModule>
# ============================================
# 3. COMPRESSION (STANDARD)
# ============================================
SetEnvIfNoCase Request_URI "\.gz$" no-gzip
SetEnvIfNoCase Request_URI "\.br$" no-gzip
SetEnvIfNoCase Request_URI "\.wasm$" no-gzip
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/plain
AddOutputFilterByType DEFLATE text/html
AddOutputFilterByType DEFLATE text/xml
AddOutputFilterByType DEFLATE text/css
AddOutputFilterByType DEFLATE application/xml
AddOutputFilterByType DEFLATE application/xhtml+xml
AddOutputFilterByType DEFLATE application/rss+xml
AddOutputFilterByType DEFLATE application/javascript
AddOutputFilterByType DEFLATE application/x-javascript
AddOutputFilterByType DEFLATE application/json
AddOutputFilterByType DEFLATE image/svg+xml
AddOutputFilterByType DEFLATE font/woff
AddOutputFilterByType DEFLATE font/ttf
</IfModule>
# ============================================
# 4. MIME TYPES & SPECIAL FILE HANDLING
# ============================================
AddType application/javascript .js .mjs
AddType application/wasm .wasm
AddType font/woff2 .woff2
AddType font/woff .woff
AddType image/webp .webp
<FilesMatch "soffice\.wasm\.gz$">
ForceType application/wasm
Header set Content-Encoding "gzip"
Header set Cross-Origin-Resource-Policy "cross-origin"
Header append Vary Accept-Encoding
</FilesMatch>
<FilesMatch "soffice\.data\.gz$">
ForceType application/octet-stream
Header set Content-Encoding "gzip"
Header append Vary Accept-Encoding
</FilesMatch>
# ============================================
# 5. REDIRECTS & ROUTING
# ============================================
# Canonical WWW
RewriteCond %{HTTP_HOST} ^bentopdf\.com [NC]
RewriteRule ^(.*)$ https://www.bentopdf.com/$1 [L,R=301]
# Force HTTPS
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
# Remove trailing slash (except for language root directories)
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_URI} !^/(ar|be|da|de|es|fr|id|it|ja|ko|nl|pl|pt|ru|sk|sv|tr|uk|vi|zh|zh-TW)/$
RewriteCond %{REQUEST_URI} (.+)/$
RewriteRule ^ %1 [R=301,L]
# Existing files/dirs - serve directly
RewriteCond %{REQUEST_FILENAME} -f [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^ - [L]
# ============================================
# 5.1. LANGUAGE ROUTES (MUST BE BEFORE .html extension rule)
# ============================================
# English prefix redirects to root
RewriteRule ^en/?$ / [R=301,L]
RewriteRule ^en/(.+)$ /$1 [R=301,L]
# Language prefix root (e.g., /de/ -> /de/index.html)
RewriteCond %{DOCUMENT_ROOT}/$1/index.html -f
RewriteRule ^(ar|be|da|de|es|fr|id|it|ja|ko|nl|pl|pt|ru|sk|sv|tr|uk|vi|zh|zh-TW)/?$ /$1/index.html [L]
# Language prefix with path (e.g., /de/merge-pdf -> /de/merge-pdf.html)
RewriteCond %{DOCUMENT_ROOT}/$1/$2.html -f
RewriteRule ^(ar|be|da|de|es|fr|id|it|ja|ko|nl|pl|pt|ru|sk|sv|tr|uk|vi|zh|zh-TW)/([^/]+)/?$ /$1/$2.html [L]
# ============================================
# 5.5. DOCS ROUTING (VitePress)
# ============================================
RewriteCond %{REQUEST_URI} ^/docs
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME}\.html -f
RewriteRule ^(.*)$ $1.html [L]
# ============================================
# 6. ADD .HTML EXTENSION IF FILE EXISTS (ROOT LEVEL ONLY)
# ============================================
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME}.html -f
RewriteRule ^([^/]+)$ $1.html [L]
# ============================================
# 7. ERROR PAGES
# ============================================
ErrorDocument 404 /404.html
-1
View File
@@ -1 +0,0 @@
npx lint-staged
-7
View File
@@ -1,7 +0,0 @@
# Handlebars partials with template syntax inside HTML attributes
src/partials/footer.html
blog/posts/
public/pdfjs-viewer/
public/pdfjs-annotation-viewer/
-8
View File
@@ -1,8 +0,0 @@
{
"semi": true,
"trailingComma": "es5",
"singleQuote": true,
"printWidth": 80,
"tabWidth": 2,
"useTabs": false
}
-5
View File
@@ -1,5 +0,0 @@
DS-0002
KSV-0125
DS-0013
-1
View File
@@ -1 +0,0 @@
https://www.bentopdf.com/funding.json
-176
View File
@@ -1,176 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- Primary Meta Tags -->
<title>404 - Page Not Found | BentoPDF</title>
<meta
name="description"
content="Oops! The page you're looking for doesn't exist. Return to BentoPDF's homepage to access our free PDF tools."
/>
<meta name="robots" content="noindex, nofollow" />
<!-- Canonical URL -->
<link rel="canonical" href="https://www.bentopdf.com/404" />
<!-- Open Graph -->
<meta property="og:type" content="website" />
<meta property="og:url" content="https://www.bentopdf.com/404" />
<meta property="og:title" content="404 - Page Not Found | BentoPDF" />
<meta
property="og:description"
content="The page you're looking for doesn't exist."
/>
<meta property="og:site_name" content="BentoPDF" />
<!-- Twitter Card -->
<meta name="twitter:card" content="summary" />
<meta name="twitter:title" content="404 - Page Not Found" />
<meta
name="twitter:description"
content="The page you're looking for doesn't exist."
/>
<meta name="twitter:site" content="@BentoPDF" />
<link href="/src/css/styles.css" rel="stylesheet" />
<!-- Web App Manifest -->
<link rel="manifest" href="/site.webmanifest" />
<!-- Favicons -->
<link rel="icon" type="image/svg+xml" href="/images/favicon.svg" />
<link
rel="icon"
type="image/png"
sizes="192x192"
href="/images/favicon-192x192.png"
/>
<link
rel="icon"
type="image/png"
sizes="512x512"
href="/images/favicon-512x512.png"
/>
<link
rel="apple-touch-icon"
sizes="180x180"
href="/images/apple-touch-icon.png"
/>
<link rel="icon" href="/favicon.ico" sizes="32x32" />
</head>
<body class="antialiased bg-gray-900 text-gray-300">
{{> navbar }}
<div id="app" class="min-h-screen container mx-auto p-4 md:p-8">
<section id="404-content" class="text-center py-20 md:py-32">
<!-- 404 Number -->
<div class="mb-8">
<h1
class="text-9xl md:text-[12rem] font-bold text-indigo-500 leading-none"
>
404
</h1>
</div>
<!-- Main heading -->
<h2 class="text-3xl md:text-5xl font-bold text-white mb-4">
Page Not Found
</h2>
<!-- Description -->
<p class="text-lg md:text-xl text-gray-400 mb-8 max-w-2xl mx-auto">
Oops! The page you're looking for seems to have wandered off into the
digital void. Don't worry though, our PDF tools are still right where
you left them.
</p>
<!-- Action buttons -->
<div
class="flex flex-col sm:flex-row gap-4 justify-center items-center"
>
<a
href="index.html"
class="inline-flex items-center gap-3 pl-8 pr-2 py-2 rounded-full bg-gradient-to-b from-indigo-500 to-indigo-600 text-white font-semibold focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-900 focus:ring-indigo-400 hover:shadow-xl hover:shadow-indigo-500/30 transition-all duration-200 transform hover:-translate-y-1"
>
<span>Back to Home</span>
<span class="bg-white text-indigo-600 rounded-full p-2">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
class="w-4 h-4"
>
<path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
<polyline points="9 22 9 12 15 12 15 22" />
</svg>
</span>
</a>
<a
href="/index.html#tools-header"
class="inline-block px-8 py-3 rounded-full bg-gray-800 text-white font-semibold border border-gray-700 hover:bg-gray-700 focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-900 focus:ring-gray-700 transition-all duration-200"
>
Browse All Tools
</a>
</div>
<!-- Helpful links -->
<div class="mt-16">
<p class="text-gray-500 mb-4">Or try these popular tools:</p>
<div class="flex flex-wrap justify-center gap-3">
<a
href="/merge-pdf"
class="px-4 py-2 bg-gray-800 text-gray-300 rounded-lg hover:bg-gray-700 hover:text-white transition-colors text-sm"
>
Merge PDF
</a>
<a
href="/split-pdf"
class="px-4 py-2 bg-gray-800 text-gray-300 rounded-lg hover:bg-gray-700 hover:text-white transition-colors text-sm"
>
Split PDF
</a>
<a
href="/compress-pdf"
class="px-4 py-2 bg-gray-800 text-gray-300 rounded-lg hover:bg-gray-700 hover:text-white transition-colors text-sm"
>
Compress PDF
</a>
<a
href="/edit-pdf"
class="px-4 py-2 bg-gray-800 text-gray-300 rounded-lg hover:bg-gray-700 hover:text-white transition-colors text-sm"
>
Edit PDF
</a>
</div>
</div>
</section>
</div>
{{> footer }}
<script type="module" src="src/js/utils/lucide-init.ts"></script>
<script type="module" src="src/js/main.ts"></script>
<script type="module" src="src/js/mobileMenu.ts"></script>
<!-- JSON-LD Schema -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebPage",
"name": "404 - Page Not Found",
"description": "The page you're looking for doesn't exist on BentoPDF.",
"url": "https://www.bentopdf.com/404"
}
</script>
</body>
</html>
-140
View File
@@ -1,140 +0,0 @@
# BentoPDF Corporate Contributor License Agreement (CCLA)
Thank you for your organization's interest in contributing to BentoPDF. This Corporate Contributor License Agreement ("Agreement") documents the rights granted by corporate contributors to the Project.
By signing this Agreement, you accept and agree to the following terms and conditions for your organization's present and future Contributions submitted to the Project.
## 1. Definitions
**"You" (or "Your")** means the legal entity on behalf of which this Agreement is being entered into.
**"Contributor"** means any employee, contractor, or authorized agent of You who submits Contributions on Your behalf.
**"Contribution"** means any original work of authorship, including any modifications or additions to an existing work, that is intentionally submitted by You or any of Your Contributors to the Project for inclusion in, or documentation of, the Project. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Project or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Project for the purpose of discussing and improving the Project.
**"Project"** means the BentoPDF software project and all associated repositories, documentation, and related materials maintained at https://github.com/alam00000/bentopdf.
**"Project Owner"** means the owners and maintainers of the Project.
## 2. Grant of Copyright License
Subject to the terms and conditions of this Agreement, You hereby grant to the Project Owner and to recipients of software distributed by the Project Owner a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to:
- Reproduce, prepare derivative works of, publicly display, publicly perform, and distribute Contributions made by You and Your Contributors, and such derivative works
- Sublicense any or all of the foregoing rights to third parties
- **Relicense Contributions under any license**, including but not limited to proprietary licenses, commercial licenses, or any other open source license
This grant specifically enables the Project Owner to offer commercial licenses of the Project incorporating Your Contributions, consistent with the Project's dual licensing model (AGPL-3.0 for open source use, and a separate commercial license for proprietary use).
## 3. Grant of Patent License
Subject to the terms and conditions of this Agreement, You hereby grant to the Project Owner and to recipients of software distributed by the Project Owner a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Contribution, where such license applies only to those patent claims licensable by You that are necessarily infringed by Your Contribution(s) alone or by combination of Your Contribution(s) with the Project to which such Contribution(s) was submitted.
If any entity institutes patent litigation against You or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that Your Contribution, or the Project to which You have contributed, constitutes direct or contributory patent infringement, then any patent licenses granted to that entity under this Agreement for that Contribution or Project shall terminate as of the date such litigation is filed.
## 4. Authorized Contributors
You are responsible for:
a) Identifying the initial list of Contributors authorized to submit Contributions on Your behalf by listing them in Schedule A below
b) Keeping the list of authorized Contributors current by notifying the Project Owner when Contributors should be added or removed
c) Ensuring that all authorized Contributors are aware of and comply with the terms of this Agreement
All Contributions made by Your authorized Contributors shall be deemed to be made on Your behalf and subject to this Agreement.
## 5. Representations and Warranties
You represent and warrant that:
a) **Authority**: You are legally entitled to grant the above licenses and have the authority to bind the legal entity You represent. You have taken all necessary corporate action to authorize the execution of this Agreement.
b) **Contributor Authorization**: Each Contributor identified by You is authorized to submit Contributions on Your behalf.
c) **Originality**: Each Contribution is Your organization's original creation, or You have sufficient rights to submit the Contribution on behalf of the original authors.
d) **Third-Party Rights**: Your Contributions include complete details of any third-party license or other restriction (including, but not limited to, related patents and trademarks) of which You are aware and which are associated with any part of Your Contributions.
e) **No Conflicting Obligations**: Your Contributions do not violate any agreement or obligation You have with any third party.
f) **Accuracy**: All information You provide in connection with this Agreement and Your Contributions is accurate and complete.
## 6. Moral Rights Waiver
To the fullest extent permitted under applicable law, You hereby waive, and agree to cause Your Contributors to waive, any and all moral rights in or relating to Contributions, including without limitation:
- The right of attribution
- The right of integrity
- The right to object to derogatory treatment
- Any similar rights existing under the laws of any jurisdiction
You acknowledge that the Project Owner may modify, adapt, translate, or otherwise change Contributions without consent and without attribution.
## 7. No Revocation
**This license grant is irrevocable.** Once a Contribution has been submitted under this Agreement, You may not revoke or withdraw the licenses granted herein. The Project Owner and all downstream recipients may continue to use, distribute, modify, and sublicense Contributions indefinitely.
## 8. Retention of Copyright
You retain all right, title, and interest in and to the copyright of Your Contributions. Nothing in this Agreement is intended to transfer ownership of Your Contributions to the Project Owner. You are free to use Your Contributions for any other purpose.
## 9. No Obligation
You understand that the decision to include any Contribution in any project or source repository is entirely at the discretion of the Project Owner. The Project Owner is under no obligation to accept, use, or include any Contribution.
## 10. Support and Warranty Disclaimer
Unless required by applicable law or agreed to in writing, You provide Contributions on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE.
You are not expected to provide support for Contributions, except to the extent You desire to provide support. You may provide support for free, for a fee, or not at all.
## 11. Notification
You agree to notify the Project Owner of any facts or circumstances of which You become aware that would make any of Your representations in this Agreement inaccurate in any respect.
## 12. Governing Law
This Agreement shall be governed by and construed in accordance with the laws of India, without regard to its conflict of law provisions.
---
## Signature
By signing this Agreement, You confirm that You have the authority to bind the legal entity named below, and that entity agrees to the terms of this Corporate Contributor License Agreement.
**Legal Entity Name:** ___________________________
**Address:** ___________________________
**Point of Contact Name:** ___________________________
**Point of Contact Email:** ___________________________
**Point of Contact Phone:** ___________________________
**Signature:** ___________________________
**Title:** ___________________________
**Date:** ___________________________
---
## Schedule A: Authorized Contributors
The following individuals are authorized to submit Contributions to the Project on behalf of the above-named legal entity:
| Full Name | GitHub Username | Email Address | Date Added |
|-----------|-----------------|---------------|------------|
| | | | |
| | | | |
| | | | |
| | | | |
| | | | |
*Add additional rows as needed. To update this list, contact the Project Owner at [email protected]*
---
*This CLA is based on the Apache Corporate Contributor License Agreement and has been adapted for BentoPDF's dual licensing model.*
-65
View File
@@ -1,65 +0,0 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers of **BentoPDF** pledge to make participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
---
## Our Standards
Examples of behavior that contributes to a positive environment for our community include:
- Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the community
- Showing empathy toward other community members
Examples of unacceptable behavior include:
- The use of sexualized language or imagery and unwelcome sexual attention or advances
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others’ private information, such as a physical or electronic address, without explicit permission
- Other conduct which could reasonably be considered inappropriate in a professional setting
---
## Enforcement Responsibilities
Project maintainers of **BentoPDF** are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned with this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
---
## Reporting Guidelines
If you are subject to or witness unacceptable behavior, or have any other concerns, please report it by contacting the project team at:
**Email:** `[email protected]`
All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances.
---
## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing the project include using an official project email address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
---
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at the email above. All complaints will be reviewed and investigated promptly and fairly.
Maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent consequences as determined by other members of the project's leadership.
---
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant, version 2.1](https://www.contributor-covenant.org/version/2/1/code_of_conduct.html), available at https://www.contributor-covenant.org.
-153
View File
@@ -1,153 +0,0 @@
# Contributing to BentoPDF
First off, thank you for considering contributing to **BentoPDF**! Your help makes this project better for everyone.
This document outlines how to contribute, report issues, and get involved in the project.
---
## Contributor License Agreement (CLA)
Before we can accept your contributions, you must sign our Contributor License Agreement (CLA). This is required because BentoPDF uses a dual licensing model:
- **AGPL-3.0** for open source use
- **Commercial license** for proprietary use
The CLA ensures we can include your contributions in both versions of the project.
### For Individual Contributors
Sign our [Individual Contributor License Agreement (ICLA)](ICLA.md). When you submit your first pull request, the CLA Assistant bot will automatically ask you to sign by commenting on the PR.
### For Corporate Contributors
If you are contributing on behalf of your employer, your organization needs to sign our [Corporate Contributor License Agreement (CCLA)](CCLA.md). Please contact us at [[email protected]](mailto:[email protected]) to arrange corporate CLA signing.
### What the CLA Grants
By signing the CLA, you:
- Grant us a broad copyright license to use, modify, and relicense your contributions (including for commercial use)
- Grant a patent license for any patents covering your contribution
- Represent that you have the authority to make the contribution
- Retain full copyright ownership of your contributions
---
## 1. How to Contribute
You can contribute in several ways:
- **Reporting Bugs:** If you find a bug or unexpected behavior, please open an issue. Include steps to reproduce and any relevant screenshots or logs.
- **Feature Requests:** Suggest new features or improvements by opening an issue and describing your idea clearly.
- **Code Contributions:** Submit a pull request with new features, bug fixes, or improvements.
- **Documentation:** Help improve the README, usage examples, or guides.
- **Testing:** Help test new releases or changes to ensure stability.
---
### Issue Template
When reporting bugs, requesting features, or asking questions, please use our [issue template](.github/ISSUE_TEMPLATE/bug_feature_question.md). The template will automatically appear when you create a new issue.
**Our issue template helps you provide:**
- Clear categorization (Bug, Feature Request, or Question)
- Steps to reproduce (for bugs)
- Expected vs actual behavior
- Environment details
- Screenshots or logs
**Title Format Examples:**
- `(Bug) Text alignment incorrect on multi-line paragraphs`
- `(Feature) Add support for custom PDF metadata`
- `(Question) How to embed custom fonts?`
### Pull Request Template
When submitting code contributions, please use our [pull request template](.github/pull_request_template.md). The template will automatically appear when you create a new PR.
**Key requirements:**
- Link to the related issue (e.g., `Fixes #123`)
- Describe the type of change (bug fix, feature, breaking change)
- Explain how you tested your changes
- Complete the checklist before submitting
## 2. Getting Started with Code Contributions
1. **Fork the Repository**
```bash
git clone https://github.com/alam00000/bentopdf.git
cd bentopdf
npm install
```
2. **Create a New Branch**
```bash
git checkout -b feature/my-new-feature
```
3. **Make Your Changes**
- Follow the code style and conventions used in the project.
- Add comments where necessary.
- Update or add tests if applicable.
4. **Run Tests**
```bash
npm run test
```
5. **Commit Your Changes**
```bash
git add .
git commit -m "Add a meaningful commit message"
```
6. **Push and Submit a Pull Request**
```bash
git push origin feature/my-new-feature
```
- Open a pull request on GitHub and provide a clear description of your changes.
---
## 3. Code Style
- Follow the existing TypeScript and JavaScript conventions.
- Use `camelCase` for variables and functions.
- Keep lines reasonably short and readable.
- Comment complex logic for clarity.
---
## 4. Issues and Pull Requests
- Make sure your PR is focused and addresses a single issue or feature.
- Reference related issues in your PR description (e.g., `Closes #12`).
- Be responsive to feedback and make requested changes promptly.
---
## 5. Reporting Security Issues
If you discover a security vulnerability, please **do not** open a public issue. Instead, contact the project maintainer directly at:
**Email:** [[email protected]](mailto:[email protected])
---
## 6. Code of Conduct
All contributors are expected to follow the Code of Conduct. Be respectful and considerate in all communications.
---
Thank you for helping make **BentoPDF** a better library for everyone!
-107
View File
@@ -1,107 +0,0 @@
# Global variable declaration:
# Build to serve under Subdirectory BASE_URL if provided, eg: "ARG BASE_URL=/pdf/", otherwise leave blank: "ARG BASE_URL="
ARG BASE_URL=
# Build stage
FROM mirror.gcr.io/library/node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
COPY vendor ./vendor
ENV HUSKY=0
RUN npm config set fetch-retries 5 && \
npm config set fetch-retry-mintimeout 60000 && \
npm config set fetch-retry-maxtimeout 300000 && \
npm config set fetch-timeout 600000 && \
npm ci
COPY . .
# Build without type checking (vite build only)
# Pass SIMPLE_MODE environment variable if provided
ARG SIMPLE_MODE=false
ENV SIMPLE_MODE=$SIMPLE_MODE
ARG DISABLE_GITHUB_STARS=false
ENV DISABLE_GITHUB_STARS=$DISABLE_GITHUB_STARS
ARG COMPRESSION_MODE=all
ENV COMPRESSION_MODE=$COMPRESSION_MODE
# global arg to local arg - BASE_URL is read from env by vite.config.ts
ARG BASE_URL
ENV BASE_URL=$BASE_URL
# WASM module URLs (pre-configured defaults)
# Override these for air-gapped or self-hosted WASM deployments
ARG VITE_WASM_PYMUPDF_URL
ARG VITE_WASM_GS_URL
ARG VITE_WASM_CPDF_URL
ENV VITE_WASM_PYMUPDF_URL=$VITE_WASM_PYMUPDF_URL
ENV VITE_WASM_GS_URL=$VITE_WASM_GS_URL
ENV VITE_WASM_CPDF_URL=$VITE_WASM_CPDF_URL
# OCR asset URLs (optional, used for self-hosted or air-gapped OCR)
ARG VITE_TESSERACT_WORKER_URL
ARG VITE_TESSERACT_CORE_URL
ARG VITE_TESSERACT_LANG_URL
ARG VITE_TESSERACT_AVAILABLE_LANGUAGES
ARG VITE_OCR_FONT_BASE_URL
ENV VITE_TESSERACT_WORKER_URL=$VITE_TESSERACT_WORKER_URL
ENV VITE_TESSERACT_CORE_URL=$VITE_TESSERACT_CORE_URL
ENV VITE_TESSERACT_LANG_URL=$VITE_TESSERACT_LANG_URL
ENV VITE_TESSERACT_AVAILABLE_LANGUAGES=$VITE_TESSERACT_AVAILABLE_LANGUAGES
ENV VITE_OCR_FONT_BASE_URL=$VITE_OCR_FONT_BASE_URL
# Default UI language (e.g. en, fr, de, es, zh, ar)
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
ARG DISABLE_TOOLS
ENV DISABLE_TOOLS=$DISABLE_TOOLS
# Public-facing canonical site URL. Defaults to the official site so self-hosters
# consolidate SEO signals back to bentopdf.com. Override with --build-arg
# SITE_URL=https://your-domain.example to claim canonical for your own deployment.
ARG SITE_URL=https://www.bentopdf.com
ENV SITE_URL=$SITE_URL
ENV NODE_OPTIONS="--max-old-space-size=3072"
RUN --mount=type=secret,id=VITE_CORS_PROXY_URL,required=false \
--mount=type=secret,id=VITE_CORS_PROXY_SECRET,required=false \
VITE_CORS_PROXY_URL=$(cat /run/secrets/VITE_CORS_PROXY_URL 2>/dev/null || echo "") \
VITE_CORS_PROXY_SECRET=$(cat /run/secrets/VITE_CORS_PROXY_SECRET 2>/dev/null || echo "") \
npm run build:with-docs
# Production stage
FROM quay.io/nginx/nginx-unprivileged:alpine-slim
LABEL org.opencontainers.image.source="https://github.com/alam00000/bentopdf"
LABEL org.opencontainers.image.url="https://github.com/alam00000/bentopdf"
# global arg to local arg
ARG BASE_URL
# Set this to "true" to disable Nginx listening on IPv6
ENV DISABLE_IPV6=false
ENV PORT=8080
USER root
RUN apk upgrade --no-cache
USER nginx
COPY --chown=nginx:nginx --from=builder /app/dist /usr/share/nginx/html${BASE_URL%/}
COPY --chown=nginx:nginx nginx.conf /etc/nginx/nginx.conf
COPY --chown=nginx:nginx --from=builder /app/security-headers.conf /etc/nginx/security-headers.conf
COPY --chown=nginx:nginx --from=builder /app/security-headers-docs.conf /etc/nginx/security-headers-docs.conf
COPY --chown=nginx:nginx --chmod=755 nginx-ipv6.sh /docker-entrypoint.d/99-disable-ipv6.sh
COPY --chown=nginx:nginx --chmod=755 nginx-noindex.sh /docker-entrypoint.d/98-noindex.sh
RUN mkdir -p /etc/nginx/tmp && chown -R nginx:nginx /etc/nginx/tmp
EXPOSE 8080
CMD ["nginx", "-g", "daemon off;"]
-103
View File
@@ -1,103 +0,0 @@
# Non-root Dockerfile — supports PUID/PGID environment variables (LSIO-style)
# Usage: docker build -f Dockerfile.nonroot -t bentopdf .
# docker run -d -p 3000:8080 -e PUID=1000 -e PGID=1000 bentopdf
ARG BASE_URL=
# Build stage (identical to main Dockerfile)
FROM mirror.gcr.io/library/node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
COPY vendor ./vendor
ENV HUSKY=0
RUN npm config set fetch-retries 5 && \
npm config set fetch-retry-mintimeout 60000 && \
npm config set fetch-retry-maxtimeout 300000 && \
npm config set fetch-timeout 600000 && \
npm ci
COPY . .
ARG SIMPLE_MODE=false
ENV SIMPLE_MODE=$SIMPLE_MODE
ARG DISABLE_GITHUB_STARS=false
ENV DISABLE_GITHUB_STARS=$DISABLE_GITHUB_STARS
ARG COMPRESSION_MODE=all
ENV COMPRESSION_MODE=$COMPRESSION_MODE
ARG BASE_URL
ENV BASE_URL=$BASE_URL
ARG VITE_WASM_PYMUPDF_URL
ARG VITE_WASM_GS_URL
ARG VITE_WASM_CPDF_URL
ENV VITE_WASM_PYMUPDF_URL=$VITE_WASM_PYMUPDF_URL
ENV VITE_WASM_GS_URL=$VITE_WASM_GS_URL
ENV VITE_WASM_CPDF_URL=$VITE_WASM_CPDF_URL
ARG VITE_TESSERACT_WORKER_URL
ARG VITE_TESSERACT_CORE_URL
ARG VITE_TESSERACT_LANG_URL
ARG VITE_TESSERACT_AVAILABLE_LANGUAGES
ARG VITE_OCR_FONT_BASE_URL
ENV VITE_TESSERACT_WORKER_URL=$VITE_TESSERACT_WORKER_URL
ENV VITE_TESSERACT_CORE_URL=$VITE_TESSERACT_CORE_URL
ENV VITE_TESSERACT_LANG_URL=$VITE_TESSERACT_LANG_URL
ENV VITE_TESSERACT_AVAILABLE_LANGUAGES=$VITE_TESSERACT_AVAILABLE_LANGUAGES
ENV VITE_OCR_FONT_BASE_URL=$VITE_OCR_FONT_BASE_URL
# Default UI language (e.g. en, fr, de, es, zh, ar)
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
# Public-facing canonical site URL. See Dockerfile for details.
ARG SITE_URL=https://www.bentopdf.com
ENV SITE_URL=$SITE_URL
ENV NODE_OPTIONS="--max-old-space-size=3072"
RUN --mount=type=secret,id=VITE_CORS_PROXY_URL,required=false \
--mount=type=secret,id=VITE_CORS_PROXY_SECRET,required=false \
VITE_CORS_PROXY_URL=$(cat /run/secrets/VITE_CORS_PROXY_URL 2>/dev/null || echo "") \
VITE_CORS_PROXY_SECRET=$(cat /run/secrets/VITE_CORS_PROXY_SECRET 2>/dev/null || echo "") \
npm run build:with-docs
# Production stage — uses standard nginx (starts as root, drops to PUID/PGID)
FROM nginx:alpine-slim
LABEL org.opencontainers.image.source="https://github.com/alam00000/bentopdf"
LABEL org.opencontainers.image.url="https://github.com/alam00000/bentopdf"
ARG BASE_URL
ENV PUID=1000
ENV PGID=1000
ENV DISABLE_IPV6=false
ENV PORT=8080
RUN apk upgrade --no-cache && apk add --no-cache su-exec
COPY --from=builder /app/dist /usr/share/nginx/html${BASE_URL%/}
COPY nginx.conf /etc/nginx/nginx.conf
COPY --from=builder /app/security-headers.conf /etc/nginx/security-headers.conf
COPY --from=builder /app/security-headers-docs.conf /etc/nginx/security-headers-docs.conf
COPY --chmod=755 entrypoint.sh /entrypoint.sh
COPY --chmod=755 nginx-noindex.sh /usr/local/bin/bentopdf-noindex.sh
RUN mkdir -p /etc/nginx/tmp \
/var/cache/nginx/client_temp \
/var/cache/nginx/proxy_temp \
/var/cache/nginx/fastcgi_temp \
/var/cache/nginx/uwsgi_temp \
/var/cache/nginx/scgi_temp
EXPOSE 8080
ENTRYPOINT ["/entrypoint.sh"]
CMD ["nginx", "-g", "daemon off;"]
-100
View File
@@ -1,100 +0,0 @@
# BentoPDF Individual Contributor License Agreement (ICLA)
Thank you for your interest in contributing to BentoPDF. This Individual Contributor License Agreement ("Agreement") documents the rights granted by contributors to the Project.
By signing this Agreement, you accept and agree to the following terms and conditions for your present and future Contributions submitted to the Project.
## 1. Definitions
**"You" (or "Your")** means the individual who is signing this Agreement and submitting Contributions to the Project.
**"Contribution"** means any original work of authorship, including any modifications or additions to an existing work, that is intentionally submitted by You to the Project for inclusion in, or documentation of, the Project. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Project or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Project for the purpose of discussing and improving the Project.
**"Project"** means the BentoPDF software project and all associated repositories, documentation, and related materials maintained at https://github.com/alam00000/bentopdf.
**"Project Owner"** means the owners and maintainers of the Project.
## 2. Grant of Copyright License
Subject to the terms and conditions of this Agreement, You hereby grant to the Project Owner and to recipients of software distributed by the Project Owner a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to:
- Reproduce, prepare derivative works of, publicly display, publicly perform, and distribute Your Contributions and such derivative works
- Sublicense any or all of the foregoing rights to third parties
- **Relicense Your Contributions under any license**, including but not limited to proprietary licenses, commercial licenses, or any other open source license
This grant specifically enables the Project Owner to offer commercial licenses of the Project incorporating Your Contributions, consistent with the Project's dual licensing model (AGPL-3.0 for open source use, and a separate commercial license for proprietary use).
## 3. Grant of Patent License
Subject to the terms and conditions of this Agreement, You hereby grant to the Project Owner and to recipients of software distributed by the Project Owner a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Contribution, where such license applies only to those patent claims licensable by You that are necessarily infringed by Your Contribution(s) alone or by combination of Your Contribution(s) with the Project to which such Contribution(s) was submitted.
If any entity institutes patent litigation against You or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that Your Contribution, or the Project to which You have contributed, constitutes direct or contributory patent infringement, then any patent licenses granted to that entity under this Agreement for that Contribution or Project shall terminate as of the date such litigation is filed.
## 4. Representations and Warranties
You represent and warrant that:
a) **Authority**: You are legally entitled to grant the above licenses. If Your employer(s) has rights to intellectual property that You create, You represent that You have received permission to make Contributions on behalf of that employer, that Your employer has waived such rights for Your Contributions, or that Your employer has executed a separate Corporate CLA with the Project.
b) **Originality**: Each of Your Contributions is Your original creation. You represent that Your Contributions include complete details of any third-party license or other restriction (including, but not limited to, related patents and trademarks) of which You are aware and which are associated with any part of Your Contributions.
c) **No Conflicting Obligations**: Your Contribution does not violate any agreement or obligation You have with any third party.
d) **Accuracy**: All information You provide in connection with this Agreement and Your Contributions is accurate and complete.
## 5. Moral Rights Waiver
To the fullest extent permitted under applicable law, You hereby waive, and agree not to assert, any and all moral rights You may have in or relating to Your Contributions, including without limitation:
- The right of attribution
- The right of integrity
- The right to object to derogatory treatment
- Any similar rights existing under the laws of any jurisdiction
You acknowledge that the Project Owner may modify, adapt, translate, or otherwise change Your Contributions without Your consent and without attribution.
## 6. No Revocation
**This license grant is irrevocable.** Once You have submitted a Contribution under this Agreement, You may not revoke or withdraw the licenses granted herein. The Project Owner and all downstream recipients may continue to use, distribute, modify, and sublicense Your Contribution indefinitely.
## 7. Retention of Copyright
You retain all right, title, and interest in and to the copyright of Your Contributions. Nothing in this Agreement is intended to transfer ownership of Your Contributions to the Project Owner. You are free to use Your Contributions for any other purpose.
## 8. No Obligation
You understand that the decision to include Your Contribution in any project or source repository is entirely at the discretion of the Project Owner. The Project Owner is under no obligation to accept, use, or include any Contribution.
## 9. Support and Warranty Disclaimer
Unless required by applicable law or agreed to in writing, You provide Your Contributions on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE.
You are not expected to provide support for Your Contributions, except to the extent You desire to provide support. You may provide support for free, for a fee, or not at all.
## 10. Notification
You agree to notify the Project Owner of any facts or circumstances of which You become aware that would make any of Your representations in this Agreement inaccurate in any respect.
## 11. Governing Law
This Agreement shall be governed by and construed in accordance with the laws of India, without regard to its conflict of law provisions.
---
## Signature
By submitting a pull request or other Contribution to the Project, and by typing your name and date below (or by signing electronically via CLA Assistant), you agree to the terms of this Individual Contributor License Agreement.
**Full Legal Name:** Stephan Paternotte
**GitHub Username:** Stephan-P
**Email Address:** stephan@paternottes.net
**Date:** 20-12-2025
**Signature:** ___________________________
---
*This CLA is based on the Apache Individual Contributor License Agreement and has been adapted for BentoPDF's dual licensing model.*
-661
View File
@@ -1,661 +0,0 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<https://www.gnu.org/licenses/>.
-1316
View File
File diff suppressed because it is too large Load Diff
-317
View File
@@ -1,317 +0,0 @@
# 🚀 Release Process - Real-World Scenarios
## 📋 Common Release Scenarios
### **Scenario 1: I Just Finished a Feature and Want to Release**
**Situation:** You've completed a new feature, tested it locally, and want to release it.
**Current State:**
```bash
$ git status
On branch main
Your branch is up to date with 'origin/main'.
Changes not staged for commit:
modified: src/js/new-feature.js
modified: src/css/styles.css
modified: README.md
Untracked files:
src/js/feature-helper.js
```
**Steps:**
```bash
# 1. Commit your feature changes
git add .
git commit -m "Add new PDF watermark feature"
# 2. Choose your release type and run
npm run release # Patch: 1.0.0 → 1.0.1 (bug fixes, small improvements)
npm run release:minor # Minor: 1.0.0 → 1.1.0 (new features, backward compatible)
npm run release:major # Major: 1.0.0 → 2.0.0 (breaking changes)
```
**What Happens:**
- ✅ Your feature commit stays as-is
- ✅ Version gets bumped in `package.json` and `chart/Chart.yaml`
- ✅ New release commit is created
- ✅ Git tag is created (e.g., `v1.0.1`)
- ✅ Everything gets pushed to GitHub
- ✅ Docker image gets built and published
---
### **Scenario 2: I Have Uncommitted Changes and Want to Release**
**Situation:** You have local changes but haven't committed them yet.
**Current State:**
```bash
$ git status
Changes not staged for commit:
modified: package.json
modified: src/js/main.js
modified: README.md
```
**❌ This Will Fail:**
```bash
npm run release
# Error: Your local changes would be overwritten by merge
```
**✅ Solution Options:**
**Option A: Commit Everything First (Recommended)**
```bash
git add .
git commit -m "Add new features and improvements"
npm run release
```
**Option B: Stash Changes Temporarily**
```bash
git stash
npm run release
git stash pop # Restore your changes after release
```
**Option C: Commit Only What's Needed**
```bash
git add package.json src/js/main.js
git commit -m "Add core improvements"
npm run release
git add README.md
git commit -m "Update documentation"
```
---
### **Scenario 3: I Want to Release a Hotfix**
**Situation:** There's a critical bug in production that needs immediate fixing.
**Steps:**
```bash
# 1. Fix the bug
git add src/js/bug-fix.js
git commit -m "Fix critical PDF rendering issue"
# 2. Release as patch (bug fix)
npm run release
# This creates: 1.0.0 → 1.0.1
```
**Result:**
- ✅ Bug fix gets released immediately
- ✅ Docker image with fix is available
- ✅ Users can pull the fixed version
---
### **Scenario 4: I Want to Release a Major Update**
**Situation:** You've added significant new features that might break existing functionality.
**Steps:**
```bash
# 1. Commit all your changes
git add .
git commit -m "Add major PDF editing features and API changes"
# 2. Release as major version
npm run release:major
# This creates: 1.0.0 → 2.0.0
```
**Result:**
- ✅ Major version bump indicates breaking changes
- ✅ Users know to check compatibility
- ✅ Both old and new versions available
---
### **Scenario 5: I Want to Release Multiple Features at Once**
**Situation:** You've been working on multiple features and want to release them together.
**Steps:**
```bash
# 1. Commit all features
git add .
git commit -m "Add multiple PDF tools: watermark, encryption, and compression"
# 2. Choose appropriate release type
npm run release:minor # For new features (1.0.0 → 1.1.0)
# OR
npm run release:major # For breaking changes (1.0.0 → 2.0.0)
```
---
### **Scenario 6: I Want to Test the Release Process**
**Situation:** You want to test the release system without affecting production.
**Steps:**
```bash
# 1. Make a small test change
echo "// Test comment" >> src/js/main.js
git add src/js/main.js
git commit -m "Test release process"
# 2. Run patch release
npm run release
# This creates: 1.0.0 → 1.0.1
# 3. Verify everything works
# Check GitHub Actions, Docker Hub, etc.
# 4. If you want to undo the test release
git tag -d v1.0.1
git push origin :refs/tags/v1.0.1
git reset --hard HEAD~1
```
---
## 🎯 **Release Type Guidelines**
| Scenario | Command | Version Change | When to Use |
| ------------------- | ----------------------- | --------------- | ------------------------------------ |
| **Bug Fix** | `npm run release` | `1.0.0 → 1.0.1` | Fixing bugs, small improvements |
| **New Feature** | `npm run release:minor` | `1.0.0 → 1.1.0` | Adding features, backward compatible |
| **Breaking Change** | `npm run release:major` | `1.0.0 → 2.0.0` | API changes, major rewrites |
---
## 🔄 **What Happens After You Run a Release Command**
### **Immediate Actions (Local):**
1. **Version Update**: `package.json` version gets bumped
2. **Git Commit**: New commit created with "Release vX.X.X"
3. **Git Tag**: Tag created (e.g., `v1.0.1`)
4. **Git Push**: Everything pushed to GitHub
### **Automatic Actions (GitHub):**
1. **GitHub Actions Triggered**: Workflow starts building Docker image
2. **Docker Build**: Multi-architecture image created
3. **Docker Push**: Images pushed to Docker Hub with tags:
- `bentopdfteam/bentopdf:latest`
- `bentopdfteam/bentopdf:1.0.1`
- `bentopdfteam/bentopdf:v1.0.1`
### **End Result:**
Users can immediately pull your new version:
```bash
docker pull bentopdfteam/bentopdf:1.0.1
```
---
## 🚨 **Before You Release - Prerequisites**
### **1. Docker Hub Credentials Setup**
You need to add these secrets to your GitHub repository:
1. Go to **Settings** → **Secrets and variables** → **Actions**
2. Add these secrets:
- `DOCKER_USERNAME`: Your Docker Hub username
- `DOCKER_TOKEN`: Your Docker Hub access token
### **2. Get Docker Hub Token**
1. Go to [Docker Hub](https://hub.docker.com)
2. Account Settings → Security → New Access Token
3. Set permissions to "Read, Write, Delete"
4. Copy the token and add it to GitHub Secrets
---
## 🔧 **Troubleshooting Common Issues**
### **❌ "Your local changes would be overwritten by merge"**
**Problem:** You have uncommitted changes
**Solution:**
```bash
git add .
git commit -m "Your commit message"
npm run release
```
### **❌ "Permission denied" in GitHub Actions**
**Problem:** Missing Docker Hub credentials
**Solution:** Add `DOCKER_USERNAME` and `DOCKER_TOKEN` to GitHub Secrets
### **❌ "Tag already exists"**
**Problem:** You've run the same release before
**Solution:** This is normal! The script will skip creating duplicate tags
### **❌ GitHub Actions fails**
**Problem:** Various build issues
**Solution:**
1. Check Actions tab for detailed logs
2. Verify Docker Hub credentials
3. Check Dockerfile for syntax errors
---
## 🧪 **Testing Your Release System**
### **Quick Test:**
```bash
# Make a small change
echo "// Test" >> src/js/main.js
git add src/js/main.js
git commit -m "Test release"
npm run release
```
### **Verify Results:**
1. **GitHub Actions**: Check Actions tab for successful build
2. **Docker Hub**: Verify images are published
3. **Git Tags**: `git tag --list` should show new tag
4. **Version**: `cat package.json | grep version` should show updated version
### **Undo Test Release:**
```bash
git tag -d v1.0.1
git push origin :refs/tags/v1.0.1
git reset --hard HEAD~1
```
---
## 🎉 **That's It!**
Your release system is now ready! Just follow the scenarios above based on your situation and run the appropriate `npm run release` command.
-110
View File
@@ -1,110 +0,0 @@
# Security Configuration
## Non-Root User Support
BentoPDF now uses nginx-unprivileged for enhanced security. This follows the Principle of Least Privilege and is essential for production environments.
### Security Benefits
- **Reduced Attack Surface**: If compromised, attackers won't have root privileges
- **Compliance**: Meets security standards like SOC 2, PCI DSS
- **Kubernetes/OpenShift Compatibility**: Works with security policies that require non-root execution
- **System Protection**: Prevents system-wide damage if the application is compromised
### Usage
#### Default Configuration (nginx-unprivileged)
```bash
docker build -t bentopdf .
docker run -p 8080:8080 bentopdf
```
#### Simple Mode
```bash
# Build with simple mode enabled
docker build --build-arg SIMPLE_MODE=true -t bentopdf-simple .
# Run the container
docker run -p 8080:8080 bentopdf-simple
```
#### Kubernetes Example
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: bentopdf
spec:
template:
spec:
securityContext:
runAsNonRoot: true
runAsUser: 2000
runAsGroup: 2000
containers:
- name: bentopdf
image: bentopdf:latest
ports:
- containerPort: 8080
```
#### Docker Compose Example
```yaml
version: '3.8'
services:
bentopdf:
build:
context: .
dockerfile: Dockerfile
args:
SIMPLE_MODE: false
ports:
- '8080:8080'
security_opt:
- no-new-privileges:true
```
### Verification
To verify the container is running as non-root:
```bash
# Check the user inside the container
docker exec <container_id> whoami
# Should output: nginx
# Check the user ID
docker exec <container_id> id
# Should show UID/GID for nginx user (typically 101)
```
### Security Best Practices
1. **Use nginx-unprivileged**: Built-in non-root user with minimal privileges
2. **Regular Updates**: Keep the base image updated (currently using 1.29-alpine)
3. **Port 8080**: Use high port numbers to avoid requiring root privileges
4. **Security Scanning**: Regularly scan images for vulnerabilities
5. **Network Policies**: Implement network segmentation
### Troubleshooting
If you encounter permission issues:
1. **Check file ownership**: Ensure all application files are owned by the nginx user
2. **Verify PID directory**: Ensure `/etc/nginx/tmp/` directory exists and is writable
3. **Port binding**: Ensure port 8080 is available and not blocked by firewall
### Migration from Root
If migrating from a root-based setup:
1. Update your Dockerfile to use nginx-unprivileged base image
2. Change port mappings from 80 to 8080 in all configurations
3. Update nginx.conf to use `/etc/nginx/tmp/nginx.pid` for PID file
4. Rebuild your images with the new security settings
5. Update your deployment configurations (Kubernetes, Docker Compose, etc.)
6. Test thoroughly in a staging environment
-250
View File
@@ -1,250 +0,0 @@
# Simple Mode for BentoPDF (the Self-Hosted build)
Simple Mode is what powers the **Self-Hosted build** of BentoPDF (`bentopdf-simple`). It is **functionally identical** to the Commercial build that powers bentopdf.com — every PDF tool is present and behaves the same. It just hides the marketing chrome (hero, FAQ, testimonials, footer) that only makes sense on the public bentopdf.com site or on a commercial public-facing deployment.
> **Simple Mode is not a feature-reduced "lite" version.** Every PDF tool — merge, split, edit, sign, OCR, Office conversion, every other tool — works identically in both builds. The only difference is the marketing UI around the tools.
The Commercial build (`ghcr.io/alam00000/bentopdf:latest`) is used by bentopdf.com itself and by commercial license holders running public-facing PDF deployments where the full marketing site makes sense.
## What Simple Mode Hides
When enabled, Simple Mode hides the following bentopdf.com-specific marketing UI:
- Navigation bar
- Hero section with marketing content
- Features section
- Security/compliance section
- FAQ section
- Testimonials section
- Support section
- Footer
It also updates the page title to "PDF Tools" and makes the tools section more prominent. **No PDF tools are removed or disabled.**
## How to Enable Simple Mode
### Method 1: Using Pre-built Simple Mode Image (Recommended)
Use the pre-built Simple Mode image directly:
**Using GitHub Container Registry (Recommended):**
```bash
# Docker
docker run -p 3000:8080 ghcr.io/alam00000/bentopdf-simple:latest
# Podman
podman run -p 3000:8080 ghcr.io/alam00000/bentopdf-simple:latest
```
**Using Docker Hub:**
```bash
# Docker
docker run -p 3000:8080 bentopdfteam/bentopdf-simple:latest
# Podman
podman run -p 3000:8080 docker.io/bentopdfteam/bentopdf-simple:latest
```
Or with Docker Compose / Podman Compose:
```yaml
services:
bentopdf:
# Using GitHub Container Registry (Recommended)
image: ghcr.io/alam00000/bentopdf-simple:latest
# Or using Docker Hub
# image: bentopdfteam/bentopdf-simple:latest
container_name: bentopdf
restart: unless-stopped
ports:
- '3000:8080'
```
### Method 2: Using Docker Compose with Build
Build the image locally with Simple Mode enabled:
```bash
docker compose -f docker-compose.dev.yml build --build-arg SIMPLE_MODE=true
docker compose -f docker-compose.dev.yml up -d
```
### Method 3: Using Docker Build
Build the image with the SIMPLE_MODE build argument:
```bash
docker build --build-arg SIMPLE_MODE=true -t bentopdf-simple .
docker run -p 3000:8080 bentopdf-simple
```
### Method 4: Using npm Script (Easiest for Local Development)
Use the built-in npm script that handles everything:
```bash
npm run serve:simple
```
This command automatically:
- Sets `SIMPLE_MODE=true`
- Builds the project with Simple Mode enabled
- Serves the built files on `http://localhost:3000`
### Method 5: Using Environment Variables
Set the environment variable before building:
```bash
export SIMPLE_MODE=true
npm run build
npx serve dist -p 3000
```
## 🧪 Testing Simple Mode Locally
### Method 1: Using npm Script (Easiest for Development)
```bash
npm run serve:simple
```
This automatically builds and serves Simple Mode on `http://localhost:3000`.
### Method 2: Using Pre-built Image (Easiest for Production)
```bash
# Docker - Pull and run the Simple Mode image
docker pull ghcr.io/alam00000/bentopdf-simple:latest
docker run -p 3000:8080 ghcr.io/alam00000/bentopdf-simple:latest
# Podman
podman pull ghcr.io/alam00000/bentopdf-simple:latest
podman run -p 3000:8080 ghcr.io/alam00000/bentopdf-simple:latest
```
Open `http://localhost:3000` in your browser.
### Method 3: Build and Test Locally
```bash
# Build with simple mode
SIMPLE_MODE=true npm run build
# Serve the built files
npx serve dist -p 3000
```
Open `http://localhost:3000` in your browser.
### Method 4: Compare Both Builds Side-by-Side
```bash
# Commercial build (the bentopdf.com look)
docker run -p 3000:8080 ghcr.io/alam00000/bentopdf:latest
# Self-Hosted build (Simple Mode)
docker run -p 3001:8080 ghcr.io/alam00000/bentopdf-simple:latest
# Podman users: replace 'docker' with 'podman'
```
- Commercial build: `http://localhost:3000`
- Self-Hosted build: `http://localhost:3001`
## 🔍 What to Look For
When Simple Mode is working correctly, you should see:
- ✅ Clean "PDF Tools" header (no marketing hero section)
- ✅ "Select a tool to get started" subtitle
- ✅ Search bar for tools
- ✅ All PDF tool cards organized by category
- ❌ No navigation bar
- ❌ No hero section with "The PDF Toolkit built for privacy"
- ❌ No features, FAQ, testimonials, or footer sections
## 📦 Available Container Images
### Self-Hosted build (Simple Mode) — recommended for self-hosting
**GitHub Container Registry (Recommended):**
- `ghcr.io/alam00000/bentopdf-simple:latest`
- `ghcr.io/alam00000/bentopdf-simple:v1.0.0` (versioned)
**Docker Hub:**
- `bentopdfteam/bentopdf-simple:latest`
- `bentopdfteam/bentopdf-simple:v1.0.0` (versioned)
### Commercial build — used by bentopdf.com and commercial license holders
The full marketing site, including hero/FAQ/testimonials/footer. Pull this only if you specifically want the bentopdf.com look — for example, you're running a public-facing PDF deployment under a commercial license.
**GitHub Container Registry (Recommended):**
- `ghcr.io/alam00000/bentopdf:latest`
- `ghcr.io/alam00000/bentopdf:v1.0.0` (versioned)
**Docker Hub:**
- `bentopdfteam/bentopdf:latest`
- `bentopdfteam/bentopdf:v1.0.0` (versioned)
## 🚀 Production Deployment Examples
### Docker Compose / Podman Compose
```yaml
services:
bentopdf:
image: ghcr.io/alam00000/bentopdf-simple:latest # Recommended
# image: bentopdfteam/bentopdf-simple:latest # Alternative: Docker Hub
container_name: bentopdf
restart: unless-stopped
ports:
- '80:8080'
environment:
- PUID=1000
- PGID=1000
```
### Podman Quadlet (Linux Systemd)
Create `~/.config/containers/systemd/bentopdf-simple.container`:
```ini
[Unit]
Description=BentoPDF Simple Mode
After=network-online.target
[Container]
Image=ghcr.io/alam00000/bentopdf-simple:latest
ContainerName=bentopdf-simple
PublishPort=80:8080
AutoUpdate=registry
[Service]
Restart=always
[Install]
WantedBy=default.target
```
Enable and start:
```bash
systemctl --user daemon-reload
systemctl --user enable --now bentopdf-simple
```
## ⚠️ Important Notes
- **Pre-built images**: Use `ghcr.io/alam00000/bentopdf-simple:latest` for Simple Mode (recommended)
- **Environment variables**: `SIMPLE_MODE=true` only works during build, not runtime
- **Build-time optimization**: Simple Mode uses dead code elimination for smaller bundles
- **Same functionality**: All PDF tools work identically in both modes
-121
View File
@@ -1,121 +0,0 @@
# Hosting BentoPDF as a static website
As an alternative to running BentoPDF locally or in a Docker container, you can easily host it as a set of static web pages. Here are a few examples:
## Netlify
### Netlify - static deployment
One of the simplest ways to host BentoPDF is to create a project at [Netlify](https://www.netlify.com/) and create a static deployment. You can accomplish this by first downloading the pre-built distribution file from our [GitHub releases](https://github.com/alam00000/bentopdf/releases). Each release includes a `dist-{version}.zip` file that contains all necessary files for self-hosting.
1. Go to [BentoPDF Releases](https://github.com/alam00000/bentopdf/releases)
2. Download the latest `dist-{version}.zip` file
3. Next, if you have not already done so, create a Netlify account and log in.
4. From your Netlify projects page, add a new project and select "Deploy manually".
5. Drag and drop to upload the BentoPDF zip file you downloaded in step 2.
6. Your BentoPDF deployment should now be working! Optionally, you can go into Project Configuration and change the project name.
When a new BentoPDF release becomes available, you will need to repeat steps 1-3, then go into "Deploys" and upload the new release.
### Netlify - dynamic deployment
Alternatively, you can configure a Netlify project to automatically deploy whenever your BentoPDF is updated.
1. If you have not done so already, create a fork of BentoPDF into your own GitHub account.
2. If you have not already done so, create a Netlify account and log in.
3. From your Netlify projects page, add a new project and select 'Import an existing project'.
4. Select the GitHub button, authorize Netlify, then choose where to install the integration. You can choose 'All repositories' or 'Only select repositories' and choose your BentoPDF fork.
5. Select your repo and give it a project name. (add environment variables?) Then click the blue 'Deploy' button.
6. The Netlify build & deploy process will kick off. Once it finishes, you can click on the provided URL `https://[projectname]/netlify.app` to view your deployment of BentoPDF.
Whenever the BentoPDF source code is updated, you can sync the changes into your repo. This will kick off a new build and deploy within Netlify.
If you want to use BentoPDF's simple mode, go into Deploy Settings, then Environment Variables, and Add a variable. Add `SIMPLE_MODE` and set it to `true`. You will need to manually kick a new build to get this to take effect.
## Vercel
Vercel provides similar services to Netlify dynamic hosting.
1. If you have not done so already, create a fork of BentoPDF into your own GitHub account.
2. If you have not already done so, create a Vercel account and log in.
3. From your Vercel Overview page, select 'Add new project'
4. Under 'Import Git Repository' and then choose 'Add GitHub Account'. Follow the prompts to authorize Vercel integration. You can choose 'All repositories' or 'Only select repositories' and choose your BentoPDF fork.
5. Select 'Import' to import the repo into Vercel.
6. Under 'Framework Preset', select 'Vite' and save the settings.
7. The Vercel build & deploy process will kick off. Once it finishes, you can click on the provided link to view your deployment of BentoPDF.
Whenever the BentoPDF source code is updated, you can sync the changes into your repo. This will kick off a new build and deploy within Vercel.
If you want to use BentoPDF's simple mode, go into Project Settings, then Environment Variables, and Create a new variable. Add `SIMPLE_MODE` and set it to `true`. You will need to redeploy to get this to take effect.
## GitHub Pages
You can also host your own instance of BentoPDF using GitHub Pages. An advantage of this over the other options is you are able to do everything in GitHub without any third-party service.
1. If you have not done so already, create a fork of BentoPDF into your own GitHub account.
2. From your fork, go to `Settings->Pages`, and change the 'Source' to 'GitHub Actions'
3. Go to `Settings->Secrets and Variables > Actions`, then select 'Variables', and add the repository variable `BASE_URL`. Set the value to `/bentopdf`. _If you've renamed the repo to something other than bentopdf, put that here_.
4. Go to `Actions` in the top menu, and select 'I understand' to enable Actions
5. Within Actions, on the left, select 'Deploy static content to Pages', and then on the right select 'Run workflow', and in the dropdown, 'Run Workflow'. The action will now run to build BentoPDF and deploy it to GitHub Pages.
When the build completes, you can find the website at `https://[your-github-username]/bentopdf`
If/when you merge changes from the source BentoPDF repository, the build and deploy action will automatically be kicked off and the new version will be automatically deployed to GitHub Pages.
## SEO and search engine indexing
By default, every self-hosted BentoPDF build emits canonical URLs and an
hreflang/`x-default` set that point back to `https://www.bentopdf.com`. This
is intentional for two reasons:
1. **It consolidates SEO signals to the official site** so your instance
doesn't compete with bentopdf.com on brand searches like "bentopdf".
2. **It keeps your instance off public search results.** If you self-host
on a small VPS, a home server, or anywhere with limited bandwidth, you
probably don't want strangers from Google landing on it. With the
default canonical pointing back to the official site, search engines
treat your instance as a copy and won't surface it for organic traffic
and your bandwidth is reserved for the people you actually share the URL
with.
You have two clean overrides depending on what you want.
### Run your own SEO instance (claim canonical for yourself)
If you operate a public deployment and want it to be the indexed copy for
your audience, set `SITE_URL` at build time to your origin. The build will
emit canonicals, hreflang, and structured data pointing at your domain.
- Docker:
```sh
docker build --build-arg SITE_URL=https://pdf.example.com -t bentopdf-self .
```
- Netlify, Vercel, GitHub Pages, or any other static host: set `SITE_URL`
in your project's environment variables and re-deploy. The build script
reads it during `npm run build`.
### Stay invisible to search engines (no SEO competition either way)
Set the runtime environment variable `ROBOTS_NOINDEX=true`. The container
injects `<meta name="robots" content="noindex, follow">` into every served
HTML file at startup. The canonical link still points at the official site
by default, so when crawlers do follow internal links they still see
bentopdf.com as the authoritative source.
```sh
docker run -d -p 8080:8080 -e ROBOTS_NOINDEX=true ghcr.io/alam00000/bentopdf:latest
```
You can combine the two: build with your own `SITE_URL` and run with
`ROBOTS_NOINDEX=true` for a private deployment that's neither competing
with bentopdf.com nor advertising itself.
### Note on Simple Mode
Simple Mode (`SIMPLE_MODE=true`) controls the **UI** — it ships a stripped-down
homepage suitable for company intranets and single-purpose deployments. It
does **not** by itself control whether your instance is indexed by search
engines. A public-facing simple-mode deployment is fully indexable by default,
just like the full-mode build. If you want a simple-mode deployment hidden
from search engines, combine `SIMPLE_MODE=true` at build time with
`ROBOTS_NOINDEX=true` at runtime.
-624
View File
@@ -1,624 +0,0 @@
# 🌍 Translation Guide for BentoPDF
This guide will help you add new languages or improve existing translations for BentoPDF.
## Table of Contents
- [Overview](#overview)
- [Quick Start](#quick-start)
- [Adding a New Language](#adding-a-new-language)
- [Translation File Structure](#translation-file-structure)
- [Where Translations Are Used](#where-translations-are-used)
- [Testing Your Translations](#testing-your-translations)
- [Translation Guidelines](#translation-guidelines)
- [Common Issues](#common-issues)
---
## Overview
BentoPDF uses **i18next** for internationalization (i18n). Currently supported languages:
- **English** (`en`) - Default
- **Belarusian** (`be`)
- **German** (`de`)
- **Spanish** (`es`)
- **French** (`fr`)
- **Italian** (`it`)
- **Portuguese** (`pt`)
- **Turkish** (`tr`)
- **Vietnamese** (`vi`)
- **Indonesian** (`id`)
- **Chinese** (`zh`)
- **Traditional Chinese (Taiwan)** (`zh-TW`)
- **Korean** (`ko`)
- **Russian** (`ru`)
- **Ukrainian** (`uk`)
- **Slovak** (`sk`)
- **Arabic** (`ar`)
- **Dutch** (`nl`)
- **Danish** (`da`)
- **Swedish** (`sv`)
- **Japanese** (`ja`)
The app automatically detects the language from the URL path:
- `/` or `/en/` → English (default)
- `/de/` → German
- `/fr/` → French
- etc.
### Architecture
BentoPDF uses a **static pre-rendering** approach for SEO-optimized i18n:
1. **Build time**: `scripts/generate-i18n-pages.mjs` generates localized HTML files in `dist/{lang}/`
2. **Dev/Preview**: `languageRouterPlugin` in `vite.config.ts` handles URL rewriting
3. **Production**: Nginx serves static files directly from language directories
---
## Quick Start
**To improve existing translations:**
1. Navigate to `public/locales/{language}/common.json` and `public/locales/{language}/tools.json`
2. Find the key you want to update
3. Change the translation value
4. Save and test
**To add a new language (e.g., Japanese `ja`):**
1. Copy `public/locales/en/` to `public/locales/ja/`
2. Translate all values in both `ja/common.json` and `ja/tools.json`
3. Add Japanese to `supportedLanguages` and `languageNames` in `src/js/i18n/i18n.ts`
4. Add `'ja'` to `SUPPORTED_LANGUAGES` in `vite.config.ts`
5. Restart the dev server
6. Run `npm run build` to generate static language pages
7. Test thoroughly
---
## Adding a New Language
Let's add **Spanish** as an example:
### Step 1: Create Translation Files
```bash
# Create the directory
mkdir -p public/locales/es
# Copy the English template
cp public/locales/en/common.json public/locales/es/common.json
```
### Step 2: Translate the JSON Files
Open `public/locales/es/common.json` and translate all the values:
```json
{
"nav": {
"home": "Inicio",
"about": "Acerca de",
"contact": "Contacto",
"allTools": "Todas las herramientas"
},
"hero": {
"title": "Tu conjunto de herramientas PDF gratuito y seguro",
"subtitle": "Combina, divide, comprime y edita archivos PDF directamente en tu navegador."
}
// ... continue translating all keys
}
```
⚠️ **Important**: Only translate the **values**, NOT the keys!
✅ **Correct:**
```json
"home": "Inicio"
```
❌ **Wrong:**
```json
"inicio": "Inicio"
```
Then do the same for `public/locales/es/tools.json` to translate all tool names and descriptions.
### Step 3: Register the Language
Edit `src/js/i18n/i18n.ts`:
```typescript
// Add 'fr' to supported languages
export const supportedLanguages = ['en', 'de', 'es', 'fr', 'zh', 'vi'] as const;
export type SupportedLanguage = (typeof supportedLanguages)[number];
// Add French display name
export const languageNames: Record<SupportedLanguage, string> = {
en: 'English',
de: 'Deutsch',
fr: 'Français', // ← Add this
};
```
### Step 4: Update Vite Configuration
In `vite.config.ts`, add your language to the `SUPPORTED_LANGUAGES` array:
```typescript
const SUPPORTED_LANGUAGES = [
'en',
'de',
'es',
'zh',
'zh-TW',
'vi',
'it',
'id',
'tr',
'fr',
'pt',
'ja',
] as const;
```
> **Important**: This is required for both dev server routing and the build-time i18n generation.
### Step 5: Test Your Translation
```bash
# Restart the dev server
npm run dev
# Visit the Japanese version
# http://localhost:5173/ja/
```
### Step 6: Build and Verify Static Files
```bash
# Run build (includes i18n page generation)
npm run build
# Verify files were created
ls dist/ja/
# Should show: index.html, merge-pdf.html, etc.
```
---
## Translation File Structure
The `common.json` file is organized into logical sections:
```json
{
"nav": {
// Navigation menu items
},
"hero": {
// Homepage hero section
},
"features": {
// Features section
},
"tools": {
// Tool names and descriptions
},
"upload": {
// File upload UI
},
"settings": {
// Settings modal and keyboard shortcuts
},
"faq": {
// FAQ section
},
"footer": {
// Footer links and text
},
"compliance": {
// Security compliance information
},
"testimonials": {
// User testimonials
},
"support": {
// Support section
},
"alert": {
// Alert and error messages
}
}
```
### Key Naming Convention
- Use **camelCase** for keys: `"deletePage"` not `"delete_page"`
- Use **nested objects** for organization: `"nav.home"` is represented as:
```json
{
"nav": {
"home": "Home"
}
}
```
- Be descriptive: `"shortcutsWarning"` is better than `"warning1"`
---
## Where Translations Are Used
### 1. HTML Templates (`data-i18n` attribute)
```html
<!-- Translation key: nav.home -->
<a href="/" data-i18n="nav.home">Home</a>
```
The `data-i18n` attribute tells i18next which translation to use.
### 2. Tool Definitions
Tool names and descriptions are defined in `src/js/config/tools.ts` and use a special namespace:
```typescript
{
name: 'Merge PDF', // Used for shortcuts only
subtitle: 'Combine multiple PDFs into one file.',
}
```
In translations:
```json
{
"tools": {
"mergePdf": {
"name": "PDF zusammenführen",
"subtitle": "Mehrere PDFs in eine Datei kombinieren."
}
}
}
```
### 3. Dynamic JavaScript (`t()` function)
For translations that need to be applied dynamically:
```typescript
import { t } from './i18n/i18n';
const message = t('alert.error');
console.log(message); // "Error" or "Fehler" depending on language
```
### 4. Placeholders
For input placeholders:
```html
<input
type="text"
placeholder="Search for a tool..."
data-i18n-placeholder="tools.searchPlaceholder"
/>
```
In `common.json`:
```json
{
"tools": {
"searchPlaceholder": "Nach einem Tool suchen..."
}
}
```
---
## Testing Your Translations
### Manual Testing
1. **Start development server:**
```bash
npm run dev
```
2. **Visit each language:**
- English: `http://localhost:5173/en/`
- German: `http://localhost:5173/de/`
- Vietnamese: `http://localhost:5173/vi/`
- Indonesian: `http://localhost:5173/id/`
- Chinese: `http://localhost:5173/zh/`
- Traditional Chinese (Taiwan): `http://localhost:5173/zh-TW/`
- French: `http://localhost:5173/fr/`
- Your new language: `http://localhost:5173/es/`
3. **Check these pages:**
- Homepage (`/`)
- About page (`/about.html`)
- Contact page (`/contact.html`)
- FAQ page (`/faq.html`)
- Tool pages (e.g., `/merge-pdf.html`)
4. **Test these interactions:**
- Click the language switcher in the footer
- Navigate between pages
- Open the settings modal (click gear icon next to search)
- Try a tool to see upload messages
### Automated Checks
Check for missing translations:
```bash
# This will show any missing keys
node scripts/check-translations.js
```
_(If this script doesn't exist, you may need to create it or manually compare JSON files)_
### Browser Testing
Test in different browsers:
- Chrome/Edge
- Firefox
- Safari
---
## Translation Guidelines
### 1. Keep the Tone Consistent
BentoPDF is **friendly, clear, and professional**. Match this tone in your translations.
✅ **Good:**
```json
"hero.title": "Ihr kostenloses und sicheres PDF-Toolkit"
```
❌ **Too formal:**
```json
"hero.title": "Ihr gebührenfreies und gesichertes Werkzeug für PDF-Dokumente"
```
### 2. Preserve Formatting
Some strings contain HTML or special characters:
```json
{
"faq.analytics.answer": "No. BentoPDF does not track you at all. We do not use cookies, analytics, or any tracking scripts, not even on the live website at bentopdf.com. We never know who you are or what you do with the tools."
}
```
When translating, **keep the HTML tags intact**:
```json
{
"faq.analytics.answer": "Nein. BentoPDF verfolgt Sie in keiner Weise. Wir verwenden keine Cookies, keine Analysetools und keine Tracking-Skripte – auch nicht auf der Live-Website bentopdf.com. Wir wissen nie, wer Sie sind oder was Sie mit den Tools tun."
}
```
### 3. Handle Plurals and Gender
If your language has complex plural rules or gender distinctions, consult the [i18next pluralization guide](https://www.i18next.com/translation-function/plurals).
Example:
```json
{
"pages": "page",
"pages_plural": "pages"
}
```
### 4. Don't Translate Brand Names or Legal Terms
Keep these as-is:
- BentoPDF
- PDF
- GitHub
- Discord
- Chrome, Firefox, Safari, etc.
- Terms and Conditions
- Privacy Policy
- Licensing
### 5. Technical Terms
For technical terms, use commonly accepted translations in your language:
- "Merge" → "Fusionner" (French), "Zusammenführen" (German)
- "Split" → "Diviser" (French), "Teilen" (German)
- "Compress" → "Compresser" (French), "Komprimieren" (German)
If unsure, check how other PDF tools translate these terms in your language.
### 6. String Length
Some UI elements have limited space. Try to keep translations **similar in length** to the English version.
If a translation is much longer, test it visually to ensure it doesn't break the layout.
---
## Common Issues
### Issue: Translations Not Showing Up
**Solution:**
1. Clear your browser cache
2. Hard refresh (Ctrl+F5 or Cmd+Shift+R)
3. Check browser console for errors
4. Verify the JSON file is valid (no syntax errors)
### Issue: Some Text Still in English
**Possible causes:**
1. Missing translation key in your language file
2. Missing `data-i18n` attribute in HTML
3. Hardcoded text in JavaScript
**Solution:**
- Compare your language file with `en/common.json` to find missing keys
- Search the codebase for hardcoded strings
### Issue: JSON Syntax Error
**Symptoms:**
```
SyntaxError: Unexpected token } in JSON at position 1234
```
**Solution:**
- Use a JSON validator: https://jsonlint.com/
- Common mistakes:
- Trailing comma after last item
- Missing or extra quotes
- Unescaped quotes inside strings (use `\"`)
### Issue: Language Switcher Not Showing New Language
**Solution:**
Make sure you added the language to both arrays in `i18n.ts`:
```typescript
export const supportedLanguages = ['en', 'de', 'es', 'fr', 'zh', 'vi']; // ← Add here
export const languageNames = {
en: 'English',
de: 'Deutsch',
es: 'Español',
fr: 'Français', // ← And here
zh: '中文',
vi: 'Tiếng Việt',
};
```
### Issue: 404 Error When Accessing Language Pages
**Symptoms:**
Visiting `http://localhost:5173/ja/about.html` shows a 404 error page.
**Solution:**
You need to add your language code to `SUPPORTED_LANGUAGES` in `vite.config.ts`:
```typescript
const SUPPORTED_LANGUAGES = [
'en',
'de',
'es',
'zh',
'zh-TW',
'vi',
'it',
'id',
'tr',
'fr',
'pt',
'ja',
] as const;
```
After updating, restart the dev server:
```bash
npm run dev
```
---
## File Checklist
When adding a new language, make sure these files are updated:
- [ ] `public/locales/{lang}/common.json` - Main translation file
- [ ] `public/locales/{lang}/tools.json` - Tools translation file
- [ ] `src/js/i18n/i18n.ts` - Add to `supportedLanguages` and `languageNames`
- [ ] `vite.config.ts` - Add to `SUPPORTED_LANGUAGES` array
- [ ] Test all pages: homepage, about, contact, FAQ, tool pages
- [ ] Test settings modal and shortcuts
- [ ] Test language switcher in footer
- [ ] Verify URL routing works (`/{lang}/`)
- [ ] Run `npm run build` and verify `dist/{lang}/` folder is created
- [ ] Test that all tools load correctly
---
## Getting Help
If you have questions or need help:
1. Check existing translations in `public/locales/de/common.json` for reference
2. Open an issue on [GitHub](https://github.com/alam00000/bentopdf/issues)
3. Join our [Discord server](https://discord.gg/Bgq3Ay3f2w)
---
## Contributing Your Translation
Once you've completed a translation:
1. **Test thoroughly** (see [Testing Your Translations](#testing-your-translations))
2. **Fork the repository** on GitHub
3. **Create a new branch**: `git checkout -b add-french-translation`
4. **Commit your changes**: `git commit -m "Add French translation"`
5. **Push to your fork**: `git push origin add-french-translation`
6. **Open a Pull Request** with:
- Description of the language added
- Screenshots showing the translation in action
- Confirmation that you've tested all pages
Thank you for contributing to BentoPDF! 🎉
---
## Translation Progress
Current translation coverage:
| Language | Code | Status | Maintainer |
| ------------------- | ------- | -------------- | ---------- |
| English | `en` | ✅ Complete | Core team |
| German | `de` | ✅ Complete | Community |
| Spanish | `es` | ✅ Complete | Community |
| French | `fr` | ✅ Complete | Community |
| Italian | `it` | ✅ Complete | Community |
| Portuguese | `pt` | ✅ Complete | Community |
| Turkish | `tr` | ✅ Complete | Community |
| Vietnamese | `vi` | ✅ Complete | Community |
| Indonesian | `id` | ✅ Complete | Community |
| Chinese | `zh` | ✅ Complete | Community |
| Traditional Chinese | `zh-TW` | ✅ Complete | Community |
| Korean | `ko` | ✅ Complete | Community |
| Russian | `ru` | ✅ Complete | Community |
| Ukrainian | `uk` | ✅ Complete | Community |
| Slovak | `sk` | ✅ Complete | Community |
| Arabic | `ar` | ✅ Complete | Community |
| Dutch | `nl` | ✅ Complete | Community |
| Danish | `da` | ✅ Complete | Community |
| Swedish | `sv` | ✅ Complete | Community |
| Japanese | `ja` | ✅ Complete | Community |
| Your Language | `??` | 🚧 In Progress | You? |
---
**Last Updated**: June 2026
-375
View File
@@ -1,375 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- Primary Meta Tags -->
<title>About BentoPDF - Privacy-First Free PDF Tools | Our Story</title>
<meta
name="description"
content="Learn about BentoPDF - 100% free, privacy-first PDF toolkit. All tools run in your browser, no uploads, no signup. Fast, secure, and forever free."
/>
<meta
name="keywords"
content="about bentopdf, privacy-first pdf, free pdf tools, browser-based pdf, open source pdf"
/>
<meta name="author" content="BentoPDF" />
<meta name="robots" content="index, follow" />
<!-- Canonical URL -->
<link rel="canonical" href="https://www.bentopdf.com/about" />
<!-- Open Graph -->
<meta property="og:type" content="website" />
<meta property="og:url" content="https://www.bentopdf.com/about" />
<meta
property="og:title"
content="About BentoPDF - Privacy-First Free PDF Tools"
/>
<meta
property="og:description"
content="100% free PDF toolkit. Privacy-first, browser-based, no signups. Learn about our mission and values."
/>
<meta
property="og:image"
content="https://www.bentopdf.com/images/og-about.png"
/>
<meta property="og:site_name" content="BentoPDF" />
<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="About BentoPDF" />
<meta
name="twitter:description"
content="Privacy-first, free PDF tools that run in your browser."
/>
<meta
name="twitter:image"
content="https://www.bentopdf.com/images/twitter-about.png"
/>
<meta name="twitter:site" content="@BentoPDF" />
<link href="/src/css/styles.css" rel="stylesheet" />
<link href="/src/css/landing.css" rel="stylesheet" />
<!-- Web App Manifest -->
<link rel="manifest" href="/site.webmanifest" />
<!-- Favicons -->
<link rel="icon" type="image/svg+xml" href="/images/favicon.svg" />
<link
rel="icon"
type="image/png"
sizes="192x192"
href="/images/favicon-192x192.png"
/>
<link
rel="icon"
type="image/png"
sizes="512x512"
href="/images/favicon-512x512.png"
/>
<link
rel="apple-touch-icon"
sizes="180x180"
href="/images/apple-touch-icon.png"
/>
<link rel="icon" href="/favicon.ico" sizes="32x32" />
</head>
<body class="lp antialiased">
{{> navbar }}
<div id="app">
<section id="story-section" class="lp-sec lp-sec--first">
<div class="lp-frame">
<div class="lp-story">
<div class="lp-story-head">
<h1 class="lp-h2">
<span class="line">Hi, I'm Alam.</span>
<span class="line dim">I build BentoPDF.</span>
</h1>
<div class="lp-story-by">
<img
class="lp-avatar"
src="/images/author-alam.jpg"
alt="Alam"
width="44"
height="44"
/>
<a
href="https://github.com/alam00000"
target="_blank"
rel="me noopener noreferrer"
>GitHub</a
>
<a
href="https://www.linkedin.com/in/abdullah-alam01/"
target="_blank"
rel="me noopener noreferrer"
>LinkedIn</a
>
<a
href="https://x.com/alamdraws"
target="_blank"
rel="me noopener noreferrer"
>X</a
>
</div>
</div>
<div class="lp-story-body">
<p>
BentoPDF started off as just a simple internal tool I used for
quickly merging and splitting PDFs.
</p>
<p>
At some point, I decided to
<a
href="https://www.reddit.com/r/webdev/comments/1nr937y/i_made_bentopdf_a_privacy_first_pdf_toolkit_that/"
target="_blank"
rel="noopener noreferrer"
>share it on Reddit</a
>. Someone in the comments
<a
href="https://www.reddit.com/r/webdev/comments/1nr937y/comment/ngdeqby/"
target="_blank"
rel="noopener noreferrer"
>asked if it was open source</a
>. I thought, “Why not?” So a couple of weeks later, I
<a
href="https://github.com/alam00000/bentopdf"
target="_blank"
rel="noopener noreferrer"
>put the code on GitHub</a
>.
</p>
<p>
I posted it on
<a
href="https://www.reddit.com/r/selfhosted/comments/1o5borw/bentopdf_is_a_self_hostable_pdf_toolkit/"
target="_blank"
rel="noopener noreferrer"
>r/selfhosted</a
>
the next day, and that's when things really started to take off.
</p>
<p>
I honestly never thought BentoPDF would grow into what it is
today. It started as a little tool I made for myself, and
somehow it turned into a much bigger project used by people all
over the world.
</p>
</div>
</div>
<div class="lp-grid lp-grid--4 lp-cross">
<div class="lp-cell lp-stat lp-cell--accent lp-reveal">
<b>15K+</b><span>GitHub stars</span>
</div>
<div class="lp-cell lp-stat lp-cell--white lp-reveal">
<b>10M+</b><span>Downloads</span>
</div>
<div class="lp-cell lp-stat lp-cell--hatch lp-reveal">
<b>1.3K+</b><span>Forks</span>
</div>
<a
href="https://github.com/alam00000/bentopdf/graphs/contributors"
target="_blank"
rel="noopener noreferrer"
class="lp-cell lp-stat lp-cell--raised lp-reveal lp-stat-link"
>
<b>60+</b
><span>Contributors <i data-lucide="arrow-up-right"></i></span>
</a>
</div>
<div class="lp-story-outro">
<p>
I don't build BentoPDF alone anymore. More than 60 people have
contributed code, translations and fixes, and thousands more have
starred it, shared it, reported bugs and asked for features. Thank
you.
</p>
<p>Enough about me. Here's more about BentoPDF.</p>
</div>
</div>
</section>
<section id="mission-section" class="lp-sec lp-band">
<div class="lp-frame">
<figure class="lp-manifesto lp-reveal">
<h2 class="lp-h2">
<span class="line" data-i18n="about.mission.title">Our</span>
<span class="line dim" data-i18n="about.mission.titleDim"
>Mission</span
>
</h2>
<blockquote
class="lp-manifesto-text"
data-i18n="about.mission.description"
>
To provide the most comprehensive PDF toolbox that respects your
privacy and never asks for payment. We believe essential document
tools should be accessible to everyone, everywhere, without
barriers.
</blockquote>
</figure>
</div>
</section>
<section id="philosophy-section" class="lp-sec">
<div class="lp-frame">
<div class="lp-head lp-head--center">
<div>
<h2 class="lp-h2">
<span class="line" data-i18n="about.hero.title"
>PDF tools should be</span
>
<span class="line dim" data-i18n="about.hero.subtitle"
>fast, private, and free.</span
>
</h2>
<p class="lp-head-p" data-i18n="about.hero.noCompromises">
No compromises.
</p>
<p class="lp-head-p" data-i18n="about.philosophy.description">
Every BentoPDF tool runs locally in your browser. Your files are
never uploaded, nobody else sees your documents, and nothing
tracks what you do.
</p>
</div>
</div>
<div class="lp-grid lp-grid--3 lp-cross">
<div class="lp-cell lp-stat lp-cell--accent lp-reveal">
<b>0</b
><span data-i18n="features.noUploads.title">No Uploads</span>
</div>
<div class="lp-cell lp-stat lp-cell--white lp-reveal">
<b>0</b><span data-i18n="hero.noSignups">No Signups</span>
</div>
<div class="lp-cell lp-stat lp-cell--hatch lp-reveal">
<b>$0</b
><span data-i18n="features.foreverFree.title">Forever Free</span>
</div>
</div>
</div>
</section>
<section id="why-bentopdf" class="lp-sec lp-band">
<div class="lp-frame">
<div class="lp-head lp-head--center">
<div>
<h2 class="lp-h2">
<span class="line" data-i18n="about.whyBentopdf.title"
>Why</span
>
<span class="line dim">BentoPDF?</span>
</h2>
</div>
</div>
<div class="lp-grid lp-grid--2 lp-cross">
<div class="lp-cell lp-reveal">
<span class="lp-tile"><i data-lucide="zap"></i></span>
<h3 data-i18n="about.whyBentopdf.speed.title">Built for Speed</h3>
<p data-i18n="about.whyBentopdf.speed.description">
There's no upload or download step. Files are processed right in
your browser using WebAssembly, so you're never waiting on a
server.
</p>
</div>
<div class="lp-cell lp-reveal">
<span class="lp-tile"
><i data-lucide="badge-dollar-sign"></i
></span>
<h3 data-i18n="about.whyBentopdf.free.title">Free to Use</h3>
<p data-i18n="about.whyBentopdf.free.description">
Every tool is free, with no trials, no subscriptions and no
watermarks. Development is funded by commercial licenses.
</p>
</div>
<div class="lp-cell lp-reveal">
<span class="lp-tile"><i data-lucide="user-plus"></i></span>
<h3 data-i18n="about.whyBentopdf.noAccount.title">
No Account Required
</h3>
<p data-i18n="about.whyBentopdf.noAccount.description">
Open any tool and start right away. No email, no password, no
personal information.
</p>
</div>
<div class="lp-cell lp-reveal">
<span class="lp-tile"><i data-lucide="code-2"></i></span>
<h3 data-i18n="about.whyBentopdf.openSource.title">
Open Source
</h3>
<p data-i18n="about.whyBentopdf.openSource.description">
The code is public on GitHub under AGPL-3.0, so you can check
exactly how your files are handled. It builds on open-source
projects like PDF.js, pdf-lib, qpdf and LibreOffice.
</p>
</div>
</div>
</div>
</section>
<section id="cta-section" class="lp-sec">
<div class="lp-frame">
<div class="lp-cta">
<h2 class="lp-h2" data-i18n="about.cta.title">
Ready to get started?
</h2>
<p data-i18n="about.cta.description">
Join thousands of users who trust BentoPDF for their daily
document needs. Experience the difference that privacy and
performance can make.
</p>
<div class="lp-cta-actions">
<span class="lp-brackets">
<a
href="index.html#tools-header"
class="lp-btn lp-btn--primary"
>
<span data-i18n="about.cta.button">Explore All Tools</span>
<i data-lucide="arrow-right"></i>
</a>
</span>
<a href="https://bentopdf.com/docs/" class="lp-btn lp-btn--ghost">
<i data-lucide="book-open"></i>
<span data-i18n="nav.docs">Docs</span>
</a>
</div>
</div>
</div>
</section>
</div>
{{> footer }}
<script type="module" src="src/js/utils/lucide-init.ts"></script>
<script type="module" src="src/js/main.ts"></script>
<script type="module" src="src/js/mobileMenu.ts"></script>
<script type="module" src="src/js/landing.ts"></script>
<!-- JSON-LD Schema -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "AboutPage",
"name": "About BentoPDF",
"description": "How BentoPDF started, told by its creator Alam",
"url": "https://www.bentopdf.com/about",
"mainEntity": {
"@type": "Person",
"name": "Alam",
"url": "https://www.bentopdf.com/blog/author-alam",
"image": "https://www.bentopdf.com/images/author-alam.jpg",
"jobTitle": "Creator and maintainer of BentoPDF",
"sameAs": [
"https://github.com/alam00000",
"https://www.linkedin.com/in/abdullah-alam01/",
"https://x.com/alamdraws"
]
}
}
</script>
</body>
</html>
Binary file not shown.
-149
View File
@@ -1,149 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Alam, Creator of BentoPDF</title>
<meta
name="description"
content="Alam builds BentoPDF, the open-source, privacy-first PDF toolkit that runs entirely in your browser. Every post on this blog is written by him."
/>
<meta name="robots" content="index, follow" />
<link rel="canonical" href="https://www.bentopdf.com/blog/author-alam" />
<meta property="og:type" content="profile" />
<meta
property="og:url"
content="https://www.bentopdf.com/blog/author-alam"
/>
<meta property="og:title" content="Alam, Creator of BentoPDF" />
<meta
property="og:description"
content="The maintainer behind BentoPDF and the author of every post on this blog."
/>
<meta
property="og:image"
content="https://www.bentopdf.com/images/author-alam.jpg"
/>
<meta property="og:site_name" content="BentoPDF" />
<meta name="twitter:card" content="summary" />
<link rel="manifest" href="/site.webmanifest" />
<link rel="icon" type="image/svg+xml" href="/images/favicon.svg" />
<link rel="icon" href="/favicon.ico" sizes="32x32" />
<link href="/src/css/styles.css" rel="stylesheet" />
</head>
<body class="antialiased bg-gray-900">
{{> navbar }}
<section class="max-w-3xl mx-auto px-4 py-12">
<div class="flex items-center gap-5 mb-8">
<img
src="/images/author-alam.jpg"
alt="Alam"
width="80"
height="80"
class="rounded-full"
/>
<div>
<h1 class="text-3xl font-bold text-white">Alam</h1>
<p class="text-gray-400">Creator and maintainer of BentoPDF</p>
</div>
</div>
<p class="text-gray-300 leading-relaxed mb-5">
I’m Alam, and I build
<a href="/" class="text-indigo-300 hover:text-indigo-400">BentoPDF</a>,
a free, open-source PDF toolkit with 100+ tools that run entirely in
your browser.
</p>
<p class="text-gray-300 leading-relaxed mb-5">
There’s no uploading your files to a server, no account to create, and
no tracking. Your documents stay on your device. The
<a
href="https://github.com/alam00000/bentopdf"
rel="noopener"
class="text-indigo-300 hover:text-indigo-400"
>source code</a
>
is public, so you can see how it works yourself. BentoPDF is
AGPL-licensed and self-hostable.
</p>
<p class="text-gray-300 leading-relaxed mb-5">
I write every post on this blog myself. I’ll write about PDFs, how they
work, things I learn while building BentoPDF, and occasionally things
that go wrong.
</p>
<p class="text-gray-300 leading-relaxed mb-5">
When I compare BentoPDF with another PDF tool, I’ll link to its
documentation so you can check what I’m saying. If there’s something
BentoPDF doesn’t do well, I’ll say that too.
</p>
<p class="text-gray-300 leading-relaxed mb-5">
And if I get something wrong, please
<a
href="https://github.com/alam00000/bentopdf/issues"
rel="noopener"
class="text-indigo-300 hover:text-indigo-400"
>open an issue</a
>. I’ll fix it and add a note to the post.
</p>
<p class="text-gray-300 leading-relaxed mb-5">
You can find me on
<a
href="https://github.com/alam00000"
rel="me noopener"
class="text-indigo-300 hover:text-indigo-400"
>GitHub</a
>,
<a
href="https://www.linkedin.com/in/abdullah-alam01/"
rel="me noopener"
class="text-indigo-300 hover:text-indigo-400"
>LinkedIn</a
>
and
<a
href="https://x.com/alamdraws"
rel="me noopener"
class="text-indigo-300 hover:text-indigo-400"
>X</a
>.
</p>
<p class="text-gray-300 leading-relaxed">
<a
href="/blog"
class="inline-flex items-center gap-2 text-indigo-300 hover:text-indigo-400 font-semibold"
>Read the blog <i class="ph ph-arrow-right" aria-hidden="true"></i
></a>
</p>
</section>
{{> footer }}
<script type="module" src="/src/js/utils/lucide-init.ts"></script>
<script type="module">
import '@phosphor-icons/web/regular';
</script>
<script type="module" src="/src/js/mobileMenu.ts"></script>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "ProfilePage",
"mainEntity": {
"@type": "Person",
"name": "Alam",
"url": "https://www.bentopdf.com/blog/author-alam",
"image": "https://www.bentopdf.com/images/author-alam.jpg",
"jobTitle": "Creator and maintainer of BentoPDF",
"sameAs": [
"https://github.com/alam00000",
"https://www.linkedin.com/in/abdullah-alam01/",
"https://x.com/alamdraws"
]
}
}
</script>
</body>
</html>
-473
View File
@@ -1,473 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>
How to Edit a PDF Without Uploading It Anywhere | BentoPDF Blog
</title>
<meta
name="description"
content="Edit PDF text, fill forms, redact, and sign in your browser while the file stays on your device. Step by step, with the right tool for each job."
/>
<meta name="robots" content="index, follow, max-image-preview:large" />
<link
rel="canonical"
href="https://www.bentopdf.com/blog/edit-pdf-without-uploading"
/>
<meta property="og:type" content="article" />
<meta
property="og:url"
content="https://www.bentopdf.com/blog/edit-pdf-without-uploading"
/>
<meta
property="og:title"
content="How to Edit a PDF Without Uploading It Anywhere"
/>
<meta
property="og:description"
content="Edit PDF text, fill forms, redact, and sign in your browser while the file stays on your device. Step by step, with the right tool for each job."
/>
<meta
property="og:image"
content="https://www.bentopdf.com/images/og-tools.png"
/>
<meta property="og:site_name" content="BentoPDF" />
<meta name="twitter:card" content="summary_large_image" />
<link rel="manifest" href="/site.webmanifest" />
<link rel="icon" type="image/svg+xml" href="/images/favicon.svg" />
<link rel="icon" href="/favicon.ico" sizes="32x32" />
<link href="/src/css/styles.css" rel="stylesheet" />
</head>
<body class="antialiased bg-gray-900">
{{> navbar }}
<article class="max-w-3xl mx-auto px-4 py-12">
<nav
class="flex items-center gap-2 text-sm text-gray-400 mb-6"
aria-label="Breadcrumb"
>
<a href="/blog" class="hover:text-indigo-300">Blog</a>
<i class="ph ph-caret-right text-xs" aria-hidden="true"></i>
<span class="text-gray-300">Edit a PDF without uploading</span>
</nav>
<h1 class="text-3xl md:text-4xl font-bold text-white mb-4 leading-tight">
How to edit a PDF without uploading it anywhere
</h1>
<div class="flex items-center gap-3 mb-10 text-sm text-gray-400">
<img
src="/images/author-alam.jpg"
alt="Alam"
width="40"
height="40"
class="rounded-full"
/>
<div>
<a
href="/blog/author-alam"
class="text-gray-200 font-semibold hover:text-indigo-300"
>Alam</a
>, builds BentoPDF ·
<time datetime="2026-09-03">September 3, 2026</time>
</div>
</div>
<p class="text-gray-300 leading-relaxed mb-5">
You can edit a PDF in your browser without uploading the file to a
server.
</p>
<p class="text-gray-300 leading-relaxed mb-5">
Open
<a class="text-indigo-300 hover:text-indigo-400" href="/edit-pdf"
>BentoPDF&#39;s PDF editor</a
>, pick your file, make your changes, and download the result. The PDF
stays on your device. There&#39;s no account and no watermark.
</p>
<p class="text-gray-300 leading-relaxed mb-5">
But “edit a PDF” can mean a few different things. You might want to
change existing text, fill out a form, add a signature, redact
something, or just highlight a paragraph. The right tool depends on what
you&#39;re trying to do.
</p>
<h2 class="text-2xl font-bold text-white mt-10 mb-4">
Why edit a PDF without uploading it?
</h2>
<p class="text-gray-300 leading-relaxed mb-5">
Think about the PDFs you normally edit.
</p>
<p class="text-gray-300 leading-relaxed mb-5">
Contracts. Bank statements. Tax documents. Forms with your address or ID
number.
</p>
<p class="text-gray-300 leading-relaxed mb-5">
Those aren&#39;t necessarily files you want to upload to a random “free
PDF editor.”
</p>
<p class="text-gray-300 leading-relaxed mb-5">
With an on-device PDF editor, the file stays on your computer while
you&#39;re working on it.
</p>
<p class="text-gray-300 leading-relaxed mb-5">
You can even check this yourself. Open your browser&#39;s developer
tools, go to the Network tab, and watch what happens while you edit. The
PDF itself doesn&#39;t need to be sent to a server.
</p>
<h2 class="text-2xl font-bold text-white mt-10 mb-4">
How to edit the actual text in a PDF
</h2>
<p class="text-gray-300 leading-relaxed mb-5">
If you want to change words that are already in the PDF, use
<a class="text-indigo-300 hover:text-indigo-400" href="/edit-pdf-text"
>Edit PDF Text</a
>.
</p>
<ol
class="list-decimal list-inside text-gray-300 leading-relaxed mb-5 space-y-2"
>
<li>Open the tool.</li>
<li>Drop in your PDF.</li>
<li>Click on the text you want to change.</li>
<li>Type your changes.</li>
<li>Download the edited PDF.</li>
</ol>
<p class="text-gray-300 leading-relaxed mb-5">
BentoPDF loads the PDF in your browser and does the editing on your
device.
</p>
<p class="text-gray-300 leading-relaxed mb-5">
This is one of the harder things to do with PDFs because PDF text
doesn&#39;t work quite like text in a Word document. The PDF is mostly
concerned with putting things at particular positions on a page, so
changing a sentence can mean figuring out how the surrounding text
should move.
</p>
<p class="text-gray-300 leading-relaxed mb-5">
It works well for ordinary documents, but PDFs with unusual embedded
fonts or very complicated layouts can be trickier. I&#39;d always open
the downloaded PDF and check it.
</p>
<h2 class="text-2xl font-bold text-white mt-10 mb-4">
How to annotate a PDF
</h2>
<p class="text-gray-300 leading-relaxed mb-5">
If you don&#39;t need to change the existing text, annotating a PDF is
much simpler.
</p>
<p class="text-gray-300 leading-relaxed mb-5">
The
<a class="text-indigo-300 hover:text-indigo-400" href="/edit-pdf"
>PDF editor</a
>
lets you add highlights, comments, shapes, stamps, text boxes, and
images.
</p>
<p class="text-gray-300 leading-relaxed mb-5">
Open the PDF, add your annotations, and download the result.
</p>
<p class="text-gray-300 leading-relaxed mb-5">
Again, the editing happens in your browser rather than on a remote
server.
</p>
<h2 class="text-2xl font-bold text-white mt-10 mb-4">
How to fill out a PDF form
</h2>
<p class="text-gray-300 leading-relaxed mb-5">
There are two common kinds of PDF forms.
</p>
<p class="text-gray-300 leading-relaxed mb-5">
If the PDF has actual form fields, open it in
<a class="text-indigo-300 hover:text-indigo-400" href="/form-filler"
>PDF Form Filler</a
>
and type into the fields. It handles standard AcroForm fields, and also
XFA forms, which are the legacy XML-based format. XFA is worth calling
out because Chrome and most other viewers show those files as blank
pages, which is why some government forms tell you to open them in Adobe
Reader. Firefox and BentoPDF&#39;s form filler render them properly.
</p>
<p class="text-gray-300 leading-relaxed mb-5">
If it&#39;s a scanned or flattened form, there aren&#39;t any fields to
click. In that case, you can put text boxes over the blank spaces in the
<a class="text-indigo-300 hover:text-indigo-400" href="/edit-pdf"
>PDF editor</a
>
and type into them. If you&#39;d rather have real fields,
<a class="text-indigo-300 hover:text-indigo-400" href="/form-creator"
>Create PDF Form</a
>
can add them to the document first.
</p>
<p class="text-gray-300 leading-relaxed mb-5">
This is especially useful for government and HR forms, which often
contain personal information I&#39;d rather not upload somewhere just to
fill in a few boxes.
</p>
<p class="text-gray-300 leading-relaxed mb-5">
If the PDF is a scan and you actually need to edit the scanned text
itself, you&#39;ll need OCR first.
<a class="text-indigo-300 hover:text-indigo-400" href="/ocr-pdf"
>BentoPDF&#39;s OCR tool</a
>
can recognize the text in the document while keeping the processing on
your device.
</p>
<h2 class="text-2xl font-bold text-white mt-10 mb-4">
How to redact a PDF properly
</h2>
<p class="text-gray-300 leading-relaxed mb-5">
This one is important because putting a black box over text isn&#39;t
necessarily redaction.
</p>
<p class="text-gray-300 leading-relaxed mb-5">
If you draw a rectangle over a name, the original name may still exist
underneath it. It might still be selectable, searchable, or extractable.
</p>
<p class="text-gray-300 leading-relaxed mb-5">
Proper redaction removes the underlying content.
</p>
<p class="text-gray-300 leading-relaxed mb-5">
The
<a class="text-indigo-300 hover:text-indigo-400" href="/edit-pdf"
>PDF editor</a
>
has redaction tools that permanently remove the underlying content
rather than drawing over it. For documents where you also want to strip
metadata or other hidden content,
<a class="text-indigo-300 hover:text-indigo-400" href="/sanitize-pdf"
>Sanitize PDF</a
>
can help with that.
</p>
<p class="text-gray-300 leading-relaxed mb-5">
There&#39;s also a privacy reason to do redaction locally: if you upload
the original, unredacted PDF to a redaction service, that service has
already received the information you&#39;re trying to hide.
</p>
<h2 class="text-2xl font-bold text-white mt-10 mb-4">
How to sign a PDF without uploading it
</h2>
<p class="text-gray-300 leading-relaxed mb-5">
<a class="text-indigo-300 hover:text-indigo-400" href="/sign-pdf"
>Sign PDF</a
>
lets you add a drawn, typed, or image signature to a PDF. Place it on
the page, resize it, and optionally flatten it so it becomes part of the
page instead of an annotation someone can drag away later.
</p>
<p class="text-gray-300 leading-relaxed mb-5">
The signature is added on your device, so you don&#39;t need to upload
the document to a signing service just to place a signature on it.
</p>
<p class="text-gray-300 leading-relaxed mb-5">
One thing to keep in mind: adding a signature image to a PDF is not the
same thing as applying a cryptographic digital signature. If the
recipient needs a signature that can actually be verified, use
<a
class="text-indigo-300 hover:text-indigo-400"
href="/digital-sign-pdf"
>Digital Signature</a
>
instead. It signs the document with an X.509 certificate (PKCS#12 .pfx
and .p12, or PEM), which proves who signed it and shows whether anything
changed afterwards. You can also add a visible signature block on top of
that. The certificate password and private key stay in your browser,
same as the PDF.
</p>
<h2 class="text-2xl font-bold text-white mt-10 mb-4">
Can you edit a PDF offline?
</h2>
<p class="text-gray-300 leading-relaxed mb-5">Yes.</p>
<p class="text-gray-300 leading-relaxed mb-5">
There&#39;s an easy way to test it: load the editor, turn off Wi-Fi, and
edit a PDF.
</p>
<p class="text-gray-300 leading-relaxed mb-5">
The PDF processing code runs in your browser using WebAssembly. Once the
application has loaded, the actual PDF processing doesn&#39;t need a
server.
</p>
<p class="text-gray-300 leading-relaxed mb-5">
This is also something you can inspect yourself in your browser&#39;s
developer tools. The website is how you get the software; it doesn&#39;t
have to be where your PDF gets processed.
</p>
<p class="text-gray-300 leading-relaxed mb-5">
If you want to run the whole thing yourself, BentoPDF is
<a
rel="noopener"
class="text-indigo-300 hover:text-indigo-400"
href="https://github.com/alam00000/bentopdf"
>open source and self-hostable</a
>. You can run your own instance instead of relying on ours.
</p>
<h2 class="text-2xl font-bold text-white mt-10 mb-4">
What about scanned PDFs?
</h2>
<p class="text-gray-300 leading-relaxed mb-5">
A scanned PDF is basically a collection of images.
</p>
<p class="text-gray-300 leading-relaxed mb-5">
If you want to change the words in it, an ordinary PDF text editor
can&#39;t simply edit them because there isn&#39;t any text there yet.
</p>
<p class="text-gray-300 leading-relaxed mb-5">
You&#39;ll need OCR (optical character recognition) to turn the pixels
into text.
</p>
<p class="text-gray-300 leading-relaxed mb-5">
You can either OCR the PDF first and then edit the resulting text, or,
if you&#39;re just filling out a scanned form, put text boxes on top of
the existing page.
</p>
<h2 class="text-2xl font-bold text-white mt-10 mb-4">
Is there a file size limit?
</h2>
<p class="text-gray-300 leading-relaxed mb-5">
There isn&#39;t an imposed file-size limit.
</p>
<p class="text-gray-300 leading-relaxed mb-5">
The practical limit is your device&#39;s available memory. Normal PDFs
are usually fine, but a thousand-page scanned archive is a very
different workload from a five-page contract.
</p>
<p class="text-gray-300 leading-relaxed mb-5">
For very large PDFs, a desktop application may be a better choice.
</p>
<h2 class="text-2xl font-bold text-white mt-10 mb-4">
Will editing a digitally signed PDF break the signature?
</h2>
<p class="text-gray-300 leading-relaxed mb-5">Yes.</p>
<p class="text-gray-300 leading-relaxed mb-5">
Changing a digitally signed PDF will invalidate its existing signature.
This isn&#39;t specific to BentoPDF; it&#39;s a consequence of changing
a document that has already been signed.
</p>
<p class="text-gray-300 leading-relaxed mb-5">So the usual order is:</p>
<p class="text-gray-300 leading-relaxed mb-5">
<strong>Edit first. Sign last.</strong>
</p>
<p class="text-gray-300 leading-relaxed mb-5">
If you need to make changes to a signed document, make sure you
understand the signature requirements before doing so.
</p>
<div class="bg-gray-800 border border-gray-700 rounded-xl p-6 my-10">
<p class="text-white font-semibold mb-2">Edit your PDF now</p>
<p class="text-gray-400 mb-4">
Free, no signup. Your file stays on your device.
</p>
<a
href="/edit-pdf"
class="inline-block bg-indigo-600 hover:bg-indigo-700 text-white font-semibold px-5 py-2.5 rounded-lg"
>Open the PDF editor</a
>
</div>
</article>
{{> footer }}
<script type="module" src="/src/js/utils/lucide-init.ts"></script>
<script type="module" src="/src/js/mobileMenu.ts"></script>
<script type="module">
import '@phosphor-icons/web/regular';
</script>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BlogPosting",
"headline": "How to edit a PDF without uploading it anywhere",
"description": "Edit PDF text, fill forms, redact, and sign in your browser while the file stays on your device. Step by step, with the right tool for each job.",
"url": "https://www.bentopdf.com/blog/edit-pdf-without-uploading",
"datePublished": "2026-09-03",
"dateModified": "2026-09-03",
"image": "https://www.bentopdf.com/images/og-tools.png",
"author": {
"@type": "Person",
"name": "Alam",
"url": "https://www.bentopdf.com/blog/author-alam",
"sameAs": [
"https://github.com/alam00000",
"https://www.linkedin.com/in/abdullah-alam01/",
"https://x.com/alamdraws"
]
},
"publisher": {
"@type": "Organization",
"name": "BentoPDF",
"url": "https://www.bentopdf.com"
}
}
</script>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{
"@type": "ListItem",
"position": 1,
"name": "Blog",
"item": "https://www.bentopdf.com/blog/"
},
{
"@type": "ListItem",
"position": 2,
"name": "Edit a PDF without uploading",
"item": "https://www.bentopdf.com/blog/edit-pdf-without-uploading"
}
]
}
</script>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [
{
"@type": "Question",
"name": "Why edit a PDF without uploading it?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Think about the PDFs you normally edit. Contracts. Bank statements. Tax documents. Forms with your address or ID number. Those aren't necessarily files you want to upload to a random “free PDF editor.” With an on-device PDF editor, the file stays on your computer while you're working on it. You can even check this yourself. Open your browser's developer tools, go to the Network tab, and watch what happens while you edit. The PDF itself doesn't need to be sent to a server."
}
},
{
"@type": "Question",
"name": "Can you edit a PDF offline?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Yes. There's an easy way to test it: load the editor, turn off Wi-Fi, and edit a PDF. The PDF processing code runs in your browser using WebAssembly. Once the application has loaded, the actual PDF processing doesn't need a server. This is also something you can inspect yourself in your browser's developer tools. The website is how you get the software; it doesn't have to be where your PDF gets processed. If you want to run the whole thing yourself, BentoPDF is open source and self-hostable. You can run your own instance instead of relying on ours."
}
},
{
"@type": "Question",
"name": "What about scanned PDFs?",
"acceptedAnswer": {
"@type": "Answer",
"text": "A scanned PDF is basically a collection of images. If you want to change the words in it, an ordinary PDF text editor can't simply edit them because there isn't any text there yet. You'll need OCR (optical character recognition) to turn the pixels into text. You can either OCR the PDF first and then edit the resulting text, or, if you're just filling out a scanned form, put text boxes on top of the existing page."
}
},
{
"@type": "Question",
"name": "Is there a file size limit?",
"acceptedAnswer": {
"@type": "Answer",
"text": "There isn't an imposed file-size limit. The practical limit is your device's available memory. Normal PDFs are usually fine, but a thousand-page scanned archive is a very different workload from a five-page contract. For very large PDFs, a desktop application may be a better choice."
}
},
{
"@type": "Question",
"name": "Will editing a digitally signed PDF break the signature?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Yes. Changing a digitally signed PDF will invalidate its existing signature. This isn't specific to BentoPDF; it's a consequence of changing a document that has already been signed. So the usual order is: Edit first. Sign last. If you need to make changes to a signed document, make sure you understand the signature requirements before doing so."
}
}
]
}
</script>
</body>
</html>
-87
View File
@@ -1,87 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>The BentoPDF Blog | Privacy-First PDF Guides</title>
<meta
name="description"
content="Guides and honest comparisons from the maintainer of BentoPDF: how PDF tools handle your files, and how to get things done without uploading them."
/>
<meta name="robots" content="index, follow, max-image-preview:large" />
<link rel="canonical" href="https://www.bentopdf.com/blog/" />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://www.bentopdf.com/blog/" />
<meta property="og:title" content="The BentoPDF Blog" />
<meta
property="og:description"
content="Guides and honest comparisons from the maintainer of BentoPDF."
/>
<meta
property="og:image"
content="https://www.bentopdf.com/images/og-tools.png"
/>
<meta property="og:site_name" content="BentoPDF" />
<meta name="twitter:card" content="summary_large_image" />
<link rel="manifest" href="/site.webmanifest" />
<link rel="icon" type="image/svg+xml" href="/images/favicon.svg" />
<link rel="icon" href="/favicon.ico" sizes="32x32" />
<link href="/src/css/styles.css" rel="stylesheet" />
</head>
<body class="antialiased bg-gray-900">
{{> navbar }}
<section class="max-w-3xl mx-auto px-4 py-12">
<h1 class="text-3xl md:text-4xl font-bold text-white mb-4">
The BentoPDF Blog
</h1>
<p class="text-gray-300 leading-relaxed mb-2">
I'm
<a
href="/blog/author-alam"
class="text-indigo-300 hover:text-indigo-400"
>Alam</a
>. I build BentoPDF and write about PDFs, how they work, and the weird
things I run into along the way.
</p>
</section>
<section class="max-w-3xl mx-auto px-4 pb-16">
<div class="space-y-6">
<a
href="/blog/edit-pdf-without-uploading"
class="block bg-gray-800 border border-gray-700 rounded-xl p-6 hover:border-indigo-500 transition-colors"
>
<h2 class="text-xl font-bold text-white mb-2">
How to edit a PDF without uploading it anywhere
</h2>
<p class="text-gray-400 mb-3">
Rewrite text, fill forms, redact properly, and sign, locally on your
device.
</p>
<span class="text-sm text-gray-500">September 3, 2026</span>
</a>
</div>
</section>
{{> footer }}
<script type="module" src="/src/js/utils/lucide-init.ts"></script>
<script type="module" src="/src/js/mobileMenu.ts"></script>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Blog",
"name": "The BentoPDF Blog",
"url": "https://www.bentopdf.com/blog/",
"description": "Guides and honest comparisons from the maintainer of BentoPDF.",
"publisher": {
"@type": "Organization",
"name": "BentoPDF",
"url": "https://www.bentopdf.com"
}
}
</script>
</body>
</html>
-134
View File
@@ -1,134 +0,0 @@
---
{
"title": "How to Edit a PDF Without Uploading It Anywhere | BentoPDF Blog",
"h1": "How to edit a PDF without uploading it anywhere",
"ogTitle": "How to Edit a PDF Without Uploading It Anywhere",
"breadcrumb": "Edit a PDF without uploading",
"description": "Edit PDF text, fill forms, redact, and sign in your browser while the file stays on your device. Step by step, with the right tool for each job.",
"card": "Rewrite text, fill forms, redact properly, and sign, locally on your device.",
"date": "2026-09-03",
"cta": {
"heading": "Edit your PDF now",
"text": "Free, no signup. Your file stays on your device.",
"href": "/edit-pdf",
"label": "Open the PDF editor"
}
}
---
You can edit a PDF in your browser without uploading the file to a server.
Open [BentoPDF's PDF editor](/edit-pdf), pick your file, make your changes, and download the result. The PDF stays on your device. There's no account and no watermark.
But “edit a PDF” can mean a few different things. You might want to change existing text, fill out a form, add a signature, redact something, or just highlight a paragraph. The right tool depends on what you're trying to do.
## Why edit a PDF without uploading it?
Think about the PDFs you normally edit.
Contracts. Bank statements. Tax documents. Forms with your address or ID number.
Those aren't necessarily files you want to upload to a random “free PDF editor.”
With an on-device PDF editor, the file stays on your computer while you're working on it.
You can even check this yourself. Open your browser's developer tools, go to the Network tab, and watch what happens while you edit. The PDF itself doesn't need to be sent to a server.
## How to edit the actual text in a PDF
If you want to change words that are already in the PDF, use [Edit PDF Text](/edit-pdf-text).
1. Open the tool.
2. Drop in your PDF.
3. Click on the text you want to change.
4. Type your changes.
5. Download the edited PDF.
BentoPDF loads the PDF in your browser and does the editing on your device.
This is one of the harder things to do with PDFs because PDF text doesn't work quite like text in a Word document. The PDF is mostly concerned with putting things at particular positions on a page, so changing a sentence can mean figuring out how the surrounding text should move.
It works well for ordinary documents, but PDFs with unusual embedded fonts or very complicated layouts can be trickier. I'd always open the downloaded PDF and check it.
## How to annotate a PDF
If you don't need to change the existing text, annotating a PDF is much simpler.
The [PDF editor](/edit-pdf) lets you add highlights, comments, shapes, stamps, text boxes, and images.
Open the PDF, add your annotations, and download the result.
Again, the editing happens in your browser rather than on a remote server.
## How to fill out a PDF form
There are two common kinds of PDF forms.
If the PDF has actual form fields, open it in [PDF Form Filler](/form-filler) and type into the fields. It handles standard AcroForm fields, and also XFA forms, which are the legacy XML-based format. XFA is worth calling out because Chrome and most other viewers show those files as blank pages, which is why some government forms tell you to open them in Adobe Reader. Firefox and BentoPDF's form filler render them properly.
If it's a scanned or flattened form, there aren't any fields to click. In that case, you can put text boxes over the blank spaces in the [PDF editor](/edit-pdf) and type into them. If you'd rather have real fields, [Create PDF Form](/form-creator) can add them to the document first.
This is especially useful for government and HR forms, which often contain personal information I'd rather not upload somewhere just to fill in a few boxes.
If the PDF is a scan and you actually need to edit the scanned text itself, you'll need OCR first. [BentoPDF's OCR tool](/ocr-pdf) can recognize the text in the document while keeping the processing on your device.
## How to redact a PDF properly
This one is important because putting a black box over text isn't necessarily redaction.
If you draw a rectangle over a name, the original name may still exist underneath it. It might still be selectable, searchable, or extractable.
Proper redaction removes the underlying content.
The [PDF editor](/edit-pdf) has redaction tools that permanently remove the underlying content rather than drawing over it. For documents where you also want to strip metadata or other hidden content, [Sanitize PDF](/sanitize-pdf) can help with that.
There's also a privacy reason to do redaction locally: if you upload the original, unredacted PDF to a redaction service, that service has already received the information you're trying to hide.
## How to sign a PDF without uploading it
[Sign PDF](/sign-pdf) lets you add a drawn, typed, or image signature to a PDF. Place it on the page, resize it, and optionally flatten it so it becomes part of the page instead of an annotation someone can drag away later.
The signature is added on your device, so you don't need to upload the document to a signing service just to place a signature on it.
One thing to keep in mind: adding a signature image to a PDF is not the same thing as applying a cryptographic digital signature. If the recipient needs a signature that can actually be verified, use [Digital Signature](/digital-sign-pdf) instead. It signs the document with an X.509 certificate (PKCS#12 .pfx and .p12, or PEM), which proves who signed it and shows whether anything changed afterwards. You can also add a visible signature block on top of that. The certificate password and private key stay in your browser, same as the PDF.
## Can you edit a PDF offline?
Yes.
There's an easy way to test it: load the editor, turn off Wi-Fi, and edit a PDF.
The PDF processing code runs in your browser using WebAssembly. Once the application has loaded, the actual PDF processing doesn't need a server.
This is also something you can inspect yourself in your browser's developer tools. The website is how you get the software; it doesn't have to be where your PDF gets processed.
If you want to run the whole thing yourself, BentoPDF is [open source and self-hostable](https://github.com/alam00000/bentopdf). You can run your own instance instead of relying on ours.
## What about scanned PDFs?
A scanned PDF is basically a collection of images.
If you want to change the words in it, an ordinary PDF text editor can't simply edit them because there isn't any text there yet.
You'll need OCR (optical character recognition) to turn the pixels into text.
You can either OCR the PDF first and then edit the resulting text, or, if you're just filling out a scanned form, put text boxes on top of the existing page.
## Is there a file size limit?
There isn't an imposed file-size limit.
The practical limit is your device's available memory. Normal PDFs are usually fine, but a thousand-page scanned archive is a very different workload from a five-page contract.
For very large PDFs, a desktop application may be a better choice.
## Will editing a digitally signed PDF break the signature?
Yes.
Changing a digitally signed PDF will invalidate its existing signature. This isn't specific to BentoPDF; it's a consequence of changing a document that has already been signed.
So the usual order is:
**Edit first. Sign last.**
If you need to make changes to a signed document, make sure you understand the signature requirements before doing so.
-23
View File
@@ -1,23 +0,0 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/
-16
View File
@@ -1,16 +0,0 @@
apiVersion: v2
name: bentopdf
description: BentoPDF static frontend served by NGINX
icon: https://raw.githubusercontent.com/spwoodcock/bentopdf/refs/heads/main/public/favicon.ico
type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 1.0.8
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
appVersion: '2.8.9'
-100
View File
@@ -1,100 +0,0 @@
# BentoPDF Helm Chart
Deploys **BentoPDF** as a **single NGINX container** serving the static frontend.
## Quickstart
### Option 1: Port-forward (testing)
```bash
helm install bentopdf ./chart
kubectl port-forward deploy/bentopdf 8080:8080
# open http://127.0.0.1:8080
```
### Option 2: Ingress
```yaml
ingress:
enabled: true
className: nginx
hosts:
- host: bentopdf.example.com
paths:
- path: /
pathType: Prefix
```
### Option 3: Gateway API (Gateway + HTTPRoute)
```yaml
gateway:
enabled: true
gatewayClassName: 'cloudflare' # or your gateway class
httpRoute:
enabled: true
hostnames:
- pdfs.example.com
```
**Note:** Both Gateway and HTTPRoute default to the release namespace. Omit `namespace` fields to use the release namespace automatically.
If you have an existing Gateway, set `gateway.enabled=false` and configure `httpRoute.parentRefs`:
```yaml
gateway:
enabled: false
httpRoute:
enabled: true
parentRefs:
- name: existing-gateway
namespace: gateway-namespace
sectionName: http
hostnames:
- pdfs.example.com
```
## Configuration
### Image
- **`image.repository`**: container image repo (default: `ghcr.io/alam00000/bentopdf-simple`)
- **`image.tag`**: image tag (default: `Chart.appVersion`)
- **`image.pullPolicy`**: default `IfNotPresent`
### Ports
- **`containerPort`**: container listen port (**8080** for the BentoPDF nginx image)
- **`service.port`**: Service port exposed in-cluster (default **80**)
### Environment Variables
```yaml
env:
- name: DISABLE_IPV6
value: 'true'
```
## Publish this chart to GHCR (OCI) for testing/deploying
### Build And Push OCI
```bash
echo "$GHCR_TOKEN" | helm registry login ghcr.io -u "$GHCR_USERNAME" --password-stdin
cd chart
helm package .
# produces bentopdf-<version>.tgz
helm push bentopdf-*.tgz oci://ghcr.io/$GHCR_USERNAME/charts
```
This could be automated as part of a GitHub workflow.
### Deploy
```bash
helm upgrade --install bentopdf oci://ghcr.io/$GHCR_USERNAME/charts/bentopdf --version 1.0.0
```
-47
View File
@@ -1,47 +0,0 @@
1. Get the application URL by running these commands:
{{- if .Values.httpRoute.enabled }}
{{- $defaultGatewayName := .Values.gateway.name -}}
{{- if not $defaultGatewayName -}}
{{- $defaultGatewayName = printf "%s-gateway" (include "bentopdf.name" .) -}}
{{- end -}}
{{- $defaultGatewayNs := .Release.Namespace -}}
{{- if .Values.gateway.enabled -}}
{{- $defaultGatewayNs = default .Release.Namespace .Values.gateway.namespace -}}
{{- end -}}
{{- $gatewayName := $defaultGatewayName -}}
{{- $gatewayNs := $defaultGatewayNs -}}
{{- if and .Values.httpRoute.parentRefs (first .Values.httpRoute.parentRefs) -}}
{{- $firstRef := first .Values.httpRoute.parentRefs -}}
{{- $gatewayName = default $defaultGatewayName $firstRef.name -}}
{{- $gatewayNs = default $defaultGatewayNs $firstRef.namespace -}}
{{- end -}}
{{- if .Values.httpRoute.hostnames }}
export APP_HOSTNAME={{ .Values.httpRoute.hostnames | first }}
echo "Visit http://$APP_HOSTNAME to use your application"
{{- else }}
echo "HTTPRoute is enabled. Configure httpRoute.hostnames to see your URL here."
{{- end }}
NOTE: Your HTTPRoute depends on the listener configuration of your gateway and your HTTPRoute rules.
The rules can be set for path, method, header and query parameters.
You can check the gateway configuration with 'kubectl get --namespace {{ $gatewayNs }} gateway/{{ $gatewayName }} -o yaml'
{{- else if .Values.ingress.enabled }}
{{- range $host := .Values.ingress.hosts }}
{{- range .paths }}
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }}
{{- end }}
{{- end }}
{{- else if contains "NodePort" .Values.service.type }}
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "bentopdf.name" . }})
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
echo http://$NODE_IP:$NODE_PORT
{{- else if contains "LoadBalancer" .Values.service.type }}
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
You can watch its status by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "bentopdf.name" . }}'
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "bentopdf.name" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
echo http://$SERVICE_IP:{{ .Values.service.port }}
{{- else if contains "ClusterIP" .Values.service.type }}
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "bentopdf.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
echo "Visit http://127.0.0.1:8080 to use your application"
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
{{- end }}
-33
View File
@@ -1,33 +0,0 @@
{{/*
Expand the name of the bentopdf
*/}}
{{- define "bentopdf.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "bentopdf.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "bentopdf.labels" -}}
helm.sh/chart: {{ include "bentopdf.chart" . }}
{{ include "bentopdf.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "bentopdf.selectorLabels" -}}
app.kubernetes.io/name: {{ include "bentopdf.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
-74
View File
@@ -1,74 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "bentopdf.name" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "bentopdf.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "bentopdf.selectorLabels" . | nindent 6 }}
template:
metadata:
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "bentopdf.labels" . | nindent 8 }}
{{- with .Values.podLabels }}
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.podSecurityContext }}
securityContext:
{{- toYaml . | nindent 8 }}
{{- end }}
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
{{- with .Values.securityContext }}
securityContext:
{{- toYaml . | nindent 12 }}
{{- end }}
ports:
- name: http
containerPort: {{ .Values.containerPort }}
protocol: TCP
{{- with .Values.env }}
env:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.livenessProbe }}
livenessProbe:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.readinessProbe }}
readinessProbe:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.resources }}
resources:
{{- toYaml . | nindent 12 }}
{{- end }}
volumeMounts:
- name: nginx-tmp
mountPath: /etc/nginx/tmp
- name: nginx-cache
mountPath: /var/cache/nginx
- name: nginx-run
mountPath: /var/run
volumes:
- name: nginx-tmp
emptyDir: {}
- name: nginx-cache
emptyDir: {}
- name: nginx-run
emptyDir: {}
-22
View File
@@ -1,22 +0,0 @@
{{- if .Values.gateway.enabled -}}
{{- $gatewayName := .Values.gateway.name -}}
{{- if not $gatewayName -}}
{{- $gatewayName = printf "%s-gateway" (include "bentopdf.name" .) -}}
{{- end -}}
{{- $gatewayNs := default .Release.Namespace .Values.gateway.namespace -}}
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: {{ $gatewayName }}
namespace: {{ $gatewayNs }}
labels:
{{- include "bentopdf.labels" . | nindent 4 }}
{{- with .Values.gateway.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
gatewayClassName: {{ required "values.gateway.gatewayClassName is required when gateway.enabled=true" .Values.gateway.gatewayClassName }}
listeners:
{{- toYaml .Values.gateway.listeners | nindent 4 }}
{{- end }}
-52
View File
@@ -1,52 +0,0 @@
{{- if .Values.httpRoute.enabled -}}
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: {{ include "bentopdf.name" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "bentopdf.labels" . | nindent 4 }}
{{- with .Values.httpRoute.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
parentRefs:
{{- if .Values.httpRoute.parentRefs }}
{{- range $ref := .Values.httpRoute.parentRefs }}
- name: {{ $.Values.gateway.name | default $ref.name | quote }}
namespace: {{ $ref.namespace | default (default $.Release.Namespace $.Values.gateway.namespace) | quote }}
sectionName: {{ $ref.sectionName | default "http" | quote }}
{{- with $ref.kind }}
kind: {{ . | quote }}
{{- end }}
{{- with $ref.group }}
group: {{ . | quote }}
{{- end }}
{{- end }}
{{- else }}
# Default parentRef when parentRefs is empty or not set
- name: {{ .Values.gateway.name | default (printf "%s-gateway" (include "bentopdf.name" $)) | quote }}
namespace: {{ .Values.gateway.namespace | default .Release.Namespace | quote }}
sectionName: "http"
{{- end }}
{{- with .Values.httpRoute.hostnames }}
hostnames:
{{- toYaml . | nindent 4 }}
{{- end }}
rules:
{{- range .Values.httpRoute.rules }}
- {{- with .matches }}
matches:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .filters }}
filters:
{{- toYaml . | nindent 8 }}
{{- end }}
backendRefs:
- name: {{ include "bentopdf.name" $ }}
port: {{ $.Values.service.port }}
weight: 1
{{- end }}
{{- end }}
-44
View File
@@ -1,44 +0,0 @@
{{- if .Values.ingress.enabled -}}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "bentopdf.name" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "bentopdf.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- with .Values.ingress.className }}
ingressClassName: {{ . }}
{{- end }}
{{- if .Values.ingress.tls }}
tls:
{{- range .Values.ingress.tls }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
{{- with .pathType }}
pathType: {{ . }}
{{- end }}
backend:
service:
name: {{ include "bentopdf.name" $ }}
port:
number: {{ $.Values.service.port }}
{{- end }}
{{- end }}
{{- end }}
-16
View File
@@ -1,16 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "bentopdf.name" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "bentopdf.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
{{- include "bentopdf.selectorLabels" . | nindent 4 }}
@@ -1,15 +0,0 @@
apiVersion: v1
kind: Pod
metadata:
name: "{{ include "bentopdf.name" . }}-test-connection"
labels:
{{- include "bentopdf.labels" . | nindent 4 }}
annotations:
"helm.sh/hook": test
spec:
containers:
- name: wget
image: busybox
command: ['wget']
args: ['-qO-', 'http://{{ include "bentopdf.name" . }}:{{ .Values.service.port }}/']
restartPolicy: Never
-90
View File
@@ -1,90 +0,0 @@
# Default values for the BentoPDF chart (single nginx static frontend).
replicaCount: 1
image:
# Image built from this repo's `Dockerfile` (nginx serving static frontend).
repository: ghcr.io/alam00000/bentopdf-simple
pullPolicy: IfNotPresent
tag: ''
imagePullSecrets: []
nameOverride: ''
podAnnotations: {}
podLabels: {}
service:
type: ClusterIP
port: 80
# Container listen port (BentoPDF nginx image listens on 8080).
containerPort: 8080
env: []
ingress:
enabled: false
className: ''
annotations: {}
hosts:
- host: bentopdf.local
paths:
- path: /
pathType: ImplementationSpecific
tls: []
# Gateway API (optional)
gateway:
enabled: false
name: '' # default: release name + "-gateway" suffix
namespace: '' # to override the namespace
annotations: {}
gatewayClassName: '' # required when enabled=true
listeners:
- name: http
protocol: HTTP
port: 80
allowedRoutes:
namespaces:
from: Same # Allow routes from the same namespace
httpRoute:
enabled: false
annotations: {}
# parentRefs: # Omit entirely to use default (auto-references gateway in same namespace)
# - name: "" # default: gateway.name or {release-name}-gateway
# namespace: "" # to override the namespace
# sectionName: "http"
hostnames:
- bentopdf.local
rules:
- matches:
- path:
type: PathPrefix
value: /
podSecurityContext:
runAsNonRoot: true
runAsUser: 101
runAsGroup: 101
fsGroup: 101
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
seccompProfile:
type: RuntimeDefault
resources: {}
livenessProbe:
httpGet:
path: /
port: http
readinessProbe:
httpGet:
path: /
port: http
-104
View File
@@ -1,104 +0,0 @@
# WASM Proxy Setup Guide
BentoPDF uses a Cloudflare Worker to proxy WASM library requests, bypassing CORS restrictions when loading AGPL-licensed components (PyMuPDF, Ghostscript, CoherentPDF) from external sources.
## Quick Start
### 1. Deploy the Worker
```bash
cd cloudflare
npx wrangler login
npx wrangler deploy -c wasm-wrangler.toml
```
### 2. Configure Source URLs
Set environment secrets with the base URLs for your WASM files:
```bash
# Option A: Interactive prompts
npx wrangler secret put PYMUPDF_SOURCE -c wasm-wrangler.toml
npx wrangler secret put GS_SOURCE -c wasm-wrangler.toml
npx wrangler secret put CPDF_SOURCE -c wasm-wrangler.toml
# Option B: Set via Cloudflare Dashboard
# Go to Workers & Pages > bentopdf-wasm-proxy > Settings > Variables
```
**Recommended Source URLs:**
- PYMUPDF_SOURCE: `https://cdn.jsdelivr.net/npm/@bentopdf/[email protected]/`
- GS_SOURCE: `https://cdn.jsdelivr.net/npm/@bentopdf/gs-wasm/assets/`
- CPDF_SOURCE: `https://cdn.jsdelivr.net/npm/coherentpdf/dist/`
> **Note:** You can use your own hosted WASM files instead of the recommended URLs. Just ensure your files match the expected directory structure and file names that BentoPDF expects for each module.
### 3. Configure BentoPDF
**Option A: Environment variables (recommended — zero-config for users)**
Set these in `.env.production` or pass as Docker build args:
```bash
VITE_WASM_PYMUPDF_URL=https://bentopdf-wasm-proxy.<your-subdomain>.workers.dev/pymupdf/
VITE_WASM_GS_URL=https://bentopdf-wasm-proxy.<your-subdomain>.workers.dev/gs/
VITE_WASM_CPDF_URL=https://bentopdf-wasm-proxy.<your-subdomain>.workers.dev/cpdf/
```
**Option B: Manual per-user configuration**
In BentoPDF's Advanced Settings (wasm-settings.html), enter:
| Module | URL |
| ----------- | ------------------------------------------------------------------- |
| PyMuPDF | `https://bentopdf-wasm-proxy.<your-subdomain>.workers.dev/pymupdf/` |
| Ghostscript | `https://bentopdf-wasm-proxy.<your-subdomain>.workers.dev/gs/` |
| CoherentPDF | `https://bentopdf-wasm-proxy.<your-subdomain>.workers.dev/cpdf/` |
## Custom Domain (Optional)
To use a custom domain like `wasm.bentopdf.com`:
1. Add route in `wasm-wrangler.toml`:
```toml
routes = [
{ pattern = "wasm.bentopdf.com/*", zone_name = "bentopdf.com" }
]
```
2. Add DNS record in Cloudflare:
- Type: AAAA
- Name: wasm
- Content: 100::
- Proxied: Yes
3. Redeploy:
```bash
npx wrangler deploy -c wasm-wrangler.toml
```
## Security Features
- **Origin validation**: Only allows requests from configured origins
- **Rate limiting**: 100 requests/minute per IP (requires KV namespace)
- **File type restrictions**: Only WASM-related files (.js, .wasm, .data, etc.)
- **Size limits**: Max 100MB per file
- **Caching**: Reduces origin requests and improves performance
## Self-Hosting Notes
1. Update `ALLOWED_ORIGINS` in `wasm-proxy-worker.js` to include your domain
2. Host your WASM files on any origin (R2, S3, or any CDN)
3. Set source URLs as secrets in your worker
## Endpoints
| Endpoint | Description |
| ------------ | -------------------------------------- |
| `/` | Health check, shows configured modules |
| `/pymupdf/*` | PyMuPDF WASM files |
| `/gs/*` | Ghostscript WASM files |
| `/cpdf/*` | CoherentPDF files |
-581
View File
@@ -1,581 +0,0 @@
/**
* BentoPDF CORS Proxy Worker
*
* This Cloudflare Worker proxies certificate requests for the digital signing tool.
* It fetches certificates from external CAs that don't have CORS headers enabled
* and returns them with proper CORS headers.
*
*
* Deploy: npx wrangler deploy
*
* Required Environment Variables (set in wrangler.toml or Cloudflare dashboard):
* - PROXY_SECRET: Shared secret for HMAC signature verification
*/
const ALLOWED_PATH_PATTERNS = [
/\.(crt|cer|pem|der|p7c|p7b)$/i,
/(^|\/)certs(\/|$)/i,
/(^|\/)ocsp(\/|$)/i,
/(^|\/)crl(\/|$)/i,
/(^|\/)caissuers(\/|$)/i,
];
const ALLOWED_TSA_HOSTS = new Set([
'timestamp.digicert.com',
'timestamp.sectigo.com',
'ts.ssl.com',
'freetsa.org',
'tsa.mesign.com',
]);
const ALLOWED_ORIGINS = ['https://www.bentopdf.com', 'https://bentopdf.com'];
const SAFE_CONTENT_TYPES = [
'application/x-x509-ca-cert',
'application/pkix-cert',
'application/x-pem-file',
'application/pkcs7-mime',
'application/octet-stream',
'application/timestamp-reply',
'text/plain',
];
const MAX_TIMESTAMP_AGE_MS = 5 * 60 * 1000;
const RATE_LIMIT_MAX_REQUESTS = 60;
const RATE_LIMIT_WINDOW_MS = 60 * 1000;
const MAX_FILE_SIZE_BYTES = 10 * 1024 * 1024;
let warnedNoProxySecret = false;
async function verifySignature(message, signature, secret) {
try {
const encoder = new TextEncoder();
const key = await crypto.subtle.importKey(
'raw',
encoder.encode(secret),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['verify']
);
const signatureBytes = new Uint8Array(
signature.match(/.{1,2}/g).map((byte) => parseInt(byte, 16))
);
return await crypto.subtle.verify(
'HMAC',
key,
signatureBytes,
encoder.encode(message)
);
} catch (e) {
console.error('Signature verification error:', e);
return false;
}
}
function isAllowedOrigin(origin) {
if (!origin) return false;
return ALLOWED_ORIGINS.includes(origin);
}
function isPrivateOrReservedHost(hostname) {
if (
/^10\./.test(hostname) ||
/^172\.(1[6-9]|2[0-9]|3[0-1])\./.test(hostname) ||
/^192\.168\./.test(hostname) ||
/^169\.254\./.test(hostname) || // link-local (cloud metadata)
/^100\.(6[4-9]|[7-9]\d|1[0-1]\d|12[0-7])\./.test(hostname) || // CGNAT
/^127\./.test(hostname) ||
/^0\./.test(hostname)
) {
return true;
}
if (/^\d+$/.test(hostname)) {
return true;
}
const lower = hostname.toLowerCase().replace(/^\[|\]$/g, '');
if (
lower === '::1' ||
lower.startsWith('::ffff:') ||
lower.startsWith('fe80') ||
lower.startsWith('fc') ||
lower.startsWith('fd') ||
lower.startsWith('ff')
) {
return true;
}
const blockedNames = [
'localhost',
'localhost.localdomain',
'0.0.0.0',
'[::1]',
];
if (blockedNames.includes(hostname.toLowerCase())) {
return true;
}
return false;
}
async function hostnameResolvesToPrivate(hostname) {
const clean = hostname.replace(/^\[|\]$/g, '');
if (/^\d+\.\d+\.\d+\.\d+$/.test(clean) || clean.includes(':')) {
return isPrivateOrReservedHost(clean);
}
const ips = [];
for (const type of ['A', 'AAAA']) {
try {
const res = await fetch(
`https://1.1.1.1/dns-query?name=${encodeURIComponent(clean)}&type=${type}`,
{ headers: { accept: 'application/dns-json' } }
);
if (!res.ok) return true;
const data = await res.json();
for (const ans of data.Answer || []) {
if (ans && ans.data) ips.push(String(ans.data).replace(/\.$/, ''));
}
} catch {
return true;
}
}
if (ips.length === 0) return true;
return ips.some((ip) => isPrivateOrReservedHost(ip));
}
function isValidCertificateUrl(urlString) {
try {
const url = new URL(urlString);
if (!['http:', 'https:'].includes(url.protocol)) {
return false;
}
if (isPrivateOrReservedHost(url.hostname)) {
return false;
}
return ALLOWED_PATH_PATTERNS.some((pattern) => pattern.test(url.pathname));
} catch {
return false;
}
}
function isValidTsaUrl(urlString) {
try {
const url = new URL(urlString);
if (!['http:', 'https:'].includes(url.protocol)) return false;
if (isPrivateOrReservedHost(url.hostname)) return false;
return ALLOWED_TSA_HOSTS.has(url.hostname);
} catch {
return false;
}
}
function getSafeContentType(upstreamContentType) {
if (!upstreamContentType) return 'application/octet-stream';
const match = SAFE_CONTENT_TYPES.find((ct) =>
upstreamContentType.startsWith(ct)
);
return match || 'application/octet-stream';
}
function corsHeaders(origin) {
return {
'Access-Control-Allow-Origin': origin,
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
'Access-Control-Max-Age': '86400',
};
}
function handleOptions(request) {
const origin = request.headers.get('Origin');
if (!isAllowedOrigin(origin)) {
return new Response(null, { status: 403 });
}
return new Response(null, {
status: 204,
headers: corsHeaders(origin),
});
}
export default {
async fetch(request, env) {
const url = new URL(request.url);
const origin = request.headers.get('Origin');
if (request.method === 'OPTIONS') {
return handleOptions(request);
}
// NOTE: If you are selfhosting this proxy, you can remove this check, or can set it to only accept requests from your own domain
// SECURITY: The Origin allow-list and the optional PROXY_SECRET HMAC are anti-abuse controls, NOT a security boundary — the Origin
// header is forgeable by non-browser clients and the HMAC secret ships in the public client bundle. The real SSRF defense is the
// private/reserved-IP resolution check (hostnameResolvesToPrivate). Deployments running this off Cloudflare cannot fully close the
// DNS-rebinding gap in code and MUST add network egress filtering (see docs/self-hosting/cors-proxy.md).
if (!isAllowedOrigin(origin)) {
return new Response(
JSON.stringify({
error: 'Forbidden',
message: 'This proxy only accepts requests from allowed origins',
}),
{
status: 403,
headers: {
'Content-Type': 'application/json',
},
}
);
}
if (request.method !== 'GET' && request.method !== 'POST') {
return new Response('Method not allowed', {
status: 405,
headers: corsHeaders(origin),
});
}
const targetUrl = url.searchParams.get('url');
if (!targetUrl) {
return new Response(
JSON.stringify({
error: 'Missing url parameter',
usage: 'GET /?url=<certificate_url> or POST /?url=<tsa_url>',
}),
{
status: 400,
headers: {
...corsHeaders(origin),
'Content-Type': 'application/json',
},
}
);
}
const requestContentType = request.headers.get('Content-Type') || '';
const isTsaQuery =
request.method === 'POST' &&
requestContentType === 'application/timestamp-query';
if (isTsaQuery) {
if (!isValidTsaUrl(targetUrl)) {
return new Response(
JSON.stringify({
error: 'Disallowed TSA host',
message: `Only known RFC 3161 TSA hosts are accepted: ${[...ALLOWED_TSA_HOSTS].join(', ')}`,
}),
{
status: 403,
headers: {
...corsHeaders(origin),
'Content-Type': 'application/json',
},
}
);
}
} else if (!isValidCertificateUrl(targetUrl)) {
return new Response(
JSON.stringify({
error: 'Invalid or disallowed URL',
message:
'Only certificate-related URLs are allowed (*.crt, *.cer, *.pem, /certs/, /ocsp, /crl)',
}),
{
status: 403,
headers: {
...corsHeaders(origin),
'Content-Type': 'application/json',
},
}
);
}
if (env.PROXY_SECRET) {
const timestamp = url.searchParams.get('t');
const signature = url.searchParams.get('sig');
if (!timestamp || !signature) {
return new Response(
JSON.stringify({
error: 'Missing authentication parameters',
message:
'Request must include timestamp (t) and signature (sig) parameters',
}),
{
status: 401,
headers: {
...corsHeaders(origin),
'Content-Type': 'application/json',
},
}
);
}
const requestTime = parseInt(timestamp, 10);
const now = Date.now();
if (
isNaN(requestTime) ||
requestTime > now + 60000 ||
now - requestTime > MAX_TIMESTAMP_AGE_MS
) {
return new Response(
JSON.stringify({
error: 'Request expired or invalid timestamp',
message: 'Timestamp must be within 5 minutes of current time',
}),
{
status: 401,
headers: {
...corsHeaders(origin),
'Content-Type': 'application/json',
},
}
);
}
const message = `${targetUrl}${timestamp}`;
const isValid = await verifySignature(
message,
signature,
env.PROXY_SECRET
);
if (!isValid) {
return new Response(
JSON.stringify({
error: 'Invalid signature',
message: 'Request signature verification failed',
}),
{
status: 401,
headers: {
...corsHeaders(origin),
'Content-Type': 'application/json',
},
}
);
}
} else if (!warnedNoProxySecret) {
warnedNoProxySecret = true;
console.warn(
'[CORS Proxy] PROXY_SECRET is not set — request signatures are not verified, so the proxy is callable by any client that can ' +
'send an allowed Origin header. Set PROXY_SECRET to deter casual abuse (note: it ships in the public client bundle, so it is ' +
'not a real authentication boundary), and add network egress filtering if running off Cloudflare.'
);
}
const clientIP = request.headers.get('CF-Connecting-IP') || 'unknown';
const rateLimitKey = `ratelimit:${clientIP}`;
const now = Date.now();
if (env.RATE_LIMIT_KV) {
const rateLimitData = await env.RATE_LIMIT_KV.get(rateLimitKey, {
type: 'json',
});
const requests = rateLimitData?.requests || [];
const recentRequests = requests.filter(
(t) => now - t < RATE_LIMIT_WINDOW_MS
);
if (recentRequests.length >= RATE_LIMIT_MAX_REQUESTS) {
return new Response(
JSON.stringify({
error: 'Rate limit exceeded',
message: `Maximum ${RATE_LIMIT_MAX_REQUESTS} requests per minute. Please try again later.`,
retryAfter: Math.ceil(
(recentRequests[0] + RATE_LIMIT_WINDOW_MS - now) / 1000
),
}),
{
status: 429,
headers: {
...corsHeaders(origin),
'Content-Type': 'application/json',
'Retry-After': Math.ceil(
(recentRequests[0] + RATE_LIMIT_WINDOW_MS - now) / 1000
).toString(),
},
}
);
}
recentRequests.push(now);
await env.RATE_LIMIT_KV.put(
rateLimitKey,
JSON.stringify({ requests: recentRequests }),
{
expirationTtl: 120,
}
);
} else {
console.warn(
'[CORS Proxy] RATE_LIMIT_KV not configured — rate limiting is disabled'
);
}
let targetHostname;
try {
targetHostname = new URL(targetUrl).hostname;
} catch {
targetHostname = '';
}
if (!targetHostname || (await hostnameResolvesToPrivate(targetHostname))) {
return new Response(
JSON.stringify({
error: 'Forbidden destination',
message: 'The requested host is not permitted',
}),
{
status: 403,
headers: {
...corsHeaders(origin),
'Content-Type': 'application/json',
},
}
);
}
try {
const upstreamInit = {
method: request.method,
redirect: 'manual',
signal: AbortSignal.timeout(10000),
headers: {
'User-Agent': 'BentoPDF-CertProxy/1.0',
},
};
if (isTsaQuery) {
upstreamInit.headers['Content-Type'] = 'application/timestamp-query';
upstreamInit.body = await request.arrayBuffer();
}
const response = await fetch(targetUrl, upstreamInit);
if (response.status >= 300 && response.status < 400) {
return new Response(
JSON.stringify({
error: 'Redirect blocked',
message: 'Upstream redirects are not allowed',
}),
{
status: 502,
headers: {
...corsHeaders(origin),
'Content-Type': 'application/json',
},
}
);
}
if (!response.ok) {
return new Response(
JSON.stringify({
error: 'Failed to fetch certificate',
status: response.status,
}),
{
status: response.status,
headers: {
...corsHeaders(origin),
'Content-Type': 'application/json',
},
}
);
}
// Check Content-Length header first (fast reject for known-large responses)
const contentLength = parseInt(
response.headers.get('Content-Length') || '0',
10
);
if (contentLength > MAX_FILE_SIZE_BYTES) {
return new Response(
JSON.stringify({
error: 'File too large',
message: `Certificate file exceeds maximum size of ${MAX_FILE_SIZE_BYTES / 1024}KB`,
}),
{
status: 413,
headers: {
...corsHeaders(origin),
'Content-Type': 'application/json',
},
}
);
}
const reader = response.body.getReader();
const chunks = [];
let totalSize = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
totalSize += value.byteLength;
if (totalSize > MAX_FILE_SIZE_BYTES) {
reader.cancel();
return new Response(
JSON.stringify({
error: 'File too large',
message: `Certificate file exceeds maximum size of ${MAX_FILE_SIZE_BYTES / 1024}KB`,
}),
{
status: 413,
headers: {
...corsHeaders(origin),
'Content-Type': 'application/json',
},
}
);
}
chunks.push(value);
}
const certData = new Uint8Array(totalSize);
let offset = 0;
for (const chunk of chunks) {
certData.set(chunk, offset);
offset += chunk.byteLength;
}
return new Response(certData, {
status: 200,
headers: {
...corsHeaders(origin),
'Content-Type': getSafeContentType(
response.headers.get('Content-Type')
),
'Content-Length': totalSize.toString(),
'Cache-Control': 'no-store',
'X-Content-Type-Options': 'nosniff',
},
});
} catch (error) {
console.error('Proxy fetch error:', error);
return new Response(
JSON.stringify({
error: 'Proxy error',
message: 'Failed to fetch the requested certificate',
}),
{
status: 500,
headers: {
...corsHeaders(origin),
'Content-Type': 'application/json',
},
}
);
}
},
};
-356
View File
@@ -1,356 +0,0 @@
/**
* BentoPDF WASM Proxy Worker
*
* This Cloudflare Worker proxies WASM module requests to bypass CORS restrictions.
* It fetches WASM libraries (PyMuPDF, Ghostscript, CoherentPDF) from configured sources
* and serves them with proper CORS headers.
*
* Endpoints:
* - /pymupdf/* - Proxies to PyMuPDF WASM source
* - /gs/* - Proxies to Ghostscript WASM source
* - /cpdf/* - Proxies to CoherentPDF WASM source
*
* Deploy: cd cloudflare && npx wrangler deploy -c wasm-wrangler.toml
*
* Required Environment Variables (set in Cloudflare dashboard):
* - PYMUPDF_SOURCE: Base URL for PyMuPDF WASM files (e.g., https://cdn.example.com/pymupdf)
* - GS_SOURCE: Base URL for Ghostscript WASM files (e.g., https://cdn.example.com/gs)
* - CPDF_SOURCE: Base URL for CoherentPDF files (e.g., https://cdn.example.com/cpdf)
*/
const ALLOWED_ORIGINS = ['https://www.bentopdf.com', 'https://bentopdf.com'];
const MAX_FILE_SIZE_BYTES = 100 * 1024 * 1024;
const RATE_LIMIT_MAX_REQUESTS = 100;
const RATE_LIMIT_WINDOW_MS = 60 * 1000;
const CACHE_TTL_SECONDS = 604800;
const ALLOWED_EXTENSIONS = [
'.js',
'.mjs',
'.wasm',
'.data',
'.py',
'.so',
'.zip',
'.json',
'.mem',
'.asm.js',
'.worker.js',
'.html',
];
function isAllowedOrigin(origin) {
if (!origin) return true; // Allow no-origin requests (e.g., direct browser navigation)
return ALLOWED_ORIGINS.some((allowed) =>
origin.startsWith(allowed.replace(/\/$/, ''))
);
}
function isAllowedFile(pathname) {
const ext = pathname.substring(pathname.lastIndexOf('.')).toLowerCase();
if (ALLOWED_EXTENSIONS.includes(ext)) return true;
if (!pathname.includes('.') || pathname.endsWith('/')) return true;
return false;
}
function corsHeaders(origin) {
return {
'Access-Control-Allow-Origin': origin || '*',
'Access-Control-Allow-Methods': 'GET, HEAD, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Range, Cache-Control',
'Access-Control-Expose-Headers':
'Content-Length, Content-Range, Content-Type',
'Access-Control-Max-Age': '86400',
};
}
function handleOptions(request) {
const origin = request.headers.get('Origin');
return new Response(null, {
status: 204,
headers: corsHeaders(origin),
});
}
function getContentType(pathname) {
const ext = pathname.substring(pathname.lastIndexOf('.')).toLowerCase();
const contentTypes = {
'.js': 'application/javascript',
'.mjs': 'application/javascript',
'.wasm': 'application/wasm',
'.json': 'application/json',
'.data': 'application/octet-stream',
'.py': 'text/x-python',
'.so': 'application/octet-stream',
'.zip': 'application/zip',
'.mem': 'application/octet-stream',
'.html': 'text/html',
};
return contentTypes[ext] || 'application/octet-stream';
}
async function proxyRequest(request, env, sourceBaseUrl, subpath, origin) {
if (!sourceBaseUrl) {
return new Response(
JSON.stringify({
error: 'Source not configured',
message: 'This WASM module source URL has not been configured.',
}),
{
status: 503,
headers: {
...corsHeaders(origin),
'Content-Type': 'application/json',
},
}
);
}
const normalizedBase = sourceBaseUrl.endsWith('/')
? sourceBaseUrl.slice(0, -1)
: sourceBaseUrl;
const normalizedPath = subpath.startsWith('/') ? subpath : `/${subpath}`;
const targetUrl = `${normalizedBase}${normalizedPath}`;
if (!isAllowedFile(normalizedPath)) {
return new Response(
JSON.stringify({
error: 'Forbidden file type',
message: 'Only WASM-related file types are allowed.',
}),
{
status: 403,
headers: {
...corsHeaders(origin),
'Content-Type': 'application/json',
},
}
);
}
try {
const cacheKey = new Request(targetUrl, request);
const cache = caches.default;
let response = await cache.match(cacheKey);
if (!response) {
response = await fetch(targetUrl, {
headers: {
'User-Agent': 'BentoPDF-WASM-Proxy/1.0',
Accept: '*/*',
},
});
if (!response.ok) {
return new Response(
JSON.stringify({
error: 'Failed to fetch resource',
status: response.status,
statusText: response.statusText,
targetUrl: targetUrl,
}),
{
status: response.status,
headers: {
...corsHeaders(origin),
'Content-Type': 'application/json',
},
}
);
}
const contentLength = parseInt(
response.headers.get('Content-Length') || '0',
10
);
if (contentLength > MAX_FILE_SIZE_BYTES) {
return new Response(
JSON.stringify({
error: 'File too large',
message: `File exceeds maximum size of ${MAX_FILE_SIZE_BYTES / 1024 / 1024}MB`,
}),
{
status: 413,
headers: {
...corsHeaders(origin),
'Content-Type': 'application/json',
},
}
);
}
response = new Response(response.body, response);
response.headers.set(
'Cache-Control',
`public, max-age=${CACHE_TTL_SECONDS}`
);
if (response.status === 200) {
await cache.put(cacheKey, response.clone());
}
}
const bodyData = await response.arrayBuffer();
return new Response(bodyData, {
status: 200,
headers: {
...corsHeaders(origin),
'Content-Type': getContentType(normalizedPath),
'Content-Length': bodyData.byteLength.toString(),
'Cache-Control': `public, max-age=${CACHE_TTL_SECONDS}`,
'X-Proxied-From': new URL(targetUrl).hostname,
},
});
} catch (error) {
return new Response(
JSON.stringify({
error: 'Proxy error',
message: error.message,
}),
{
status: 500,
headers: {
...corsHeaders(origin),
'Content-Type': 'application/json',
},
}
);
}
}
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
const pathname = url.pathname;
const origin = request.headers.get('Origin');
if (request.method === 'OPTIONS') {
return handleOptions(request);
}
if (!isAllowedOrigin(origin)) {
return new Response(
JSON.stringify({
error: 'Forbidden',
message:
'Origin not allowed. Add your domain to ALLOWED_ORIGINS if self-hosting.',
}),
{
status: 403,
headers: {
'Content-Type': 'application/json',
...corsHeaders(origin),
},
}
);
}
if (request.method !== 'GET' && request.method !== 'HEAD') {
return new Response('Method not allowed', {
status: 405,
headers: corsHeaders(origin),
});
}
if (env.RATE_LIMIT_KV) {
const clientIP = request.headers.get('CF-Connecting-IP') || 'unknown';
const rateLimitKey = `wasm-ratelimit:${clientIP}`;
const now = Date.now();
const rateLimitData = await env.RATE_LIMIT_KV.get(rateLimitKey, {
type: 'json',
});
const requests = rateLimitData?.requests || [];
const recentRequests = requests.filter(
(t) => now - t < RATE_LIMIT_WINDOW_MS
);
if (recentRequests.length >= RATE_LIMIT_MAX_REQUESTS) {
return new Response(
JSON.stringify({
error: 'Rate limit exceeded',
message: `Maximum ${RATE_LIMIT_MAX_REQUESTS} requests per minute.`,
}),
{
status: 429,
headers: {
...corsHeaders(origin),
'Content-Type': 'application/json',
'Retry-After': '60',
},
}
);
}
recentRequests.push(now);
await env.RATE_LIMIT_KV.put(
rateLimitKey,
JSON.stringify({ requests: recentRequests }),
{
expirationTtl: 120,
}
);
}
if (pathname.startsWith('/pymupdf/')) {
const subpath = pathname.replace('/pymupdf', '');
return proxyRequest(request, env, env.PYMUPDF_SOURCE, subpath, origin);
}
if (pathname.startsWith('/gs/')) {
const subpath = pathname.replace('/gs', '');
return proxyRequest(request, env, env.GS_SOURCE, subpath, origin);
}
if (pathname.startsWith('/cpdf/')) {
const subpath = pathname.replace('/cpdf', '');
return proxyRequest(request, env, env.CPDF_SOURCE, subpath, origin);
}
if (pathname === '/' || pathname === '/health') {
return new Response(
JSON.stringify({
service: 'BentoPDF WASM Proxy',
version: '1.0.0',
endpoints: {
pymupdf: '/pymupdf/*',
gs: '/gs/*',
cpdf: '/cpdf/*',
},
configured: {
pymupdf: !!env.PYMUPDF_SOURCE,
gs: !!env.GS_SOURCE,
cpdf: !!env.CPDF_SOURCE,
},
}),
{
status: 200,
headers: {
...corsHeaders(origin),
'Content-Type': 'application/json',
},
}
);
}
return new Response(
JSON.stringify({
error: 'Not Found',
message: 'Use /pymupdf/*, /gs/*, or /cpdf/* endpoints',
}),
{
status: 404,
headers: {
...corsHeaders(origin),
'Content-Type': 'application/json',
},
}
);
},
};
-69
View File
@@ -1,69 +0,0 @@
name = "bentopdf-wasm-proxy"
main = "wasm-proxy-worker.js"
compatibility_date = "2024-01-01"
# =============================================================================
# DEPLOYMENT
# =============================================================================
# Deploy this worker:
# cd cloudflare
# npx wrangler deploy -c wasm-wrangler.toml
#
# Set environment secrets (one of the following methods):
# Option A: Cloudflare Dashboard
# Go to Workers & Pages > bentopdf-wasm-proxy > Settings > Variables
# Add: PYMUPDF_SOURCE, GS_SOURCE, CPDF_SOURCE
#
# Option B: Wrangler CLI
# npx wrangler secret put PYMUPDF_SOURCE -c wasm-wrangler.toml
# npx wrangler secret put GS_SOURCE -c wasm-wrangler.toml
# npx wrangler secret put CPDF_SOURCE -c wasm-wrangler.toml
# =============================================================================
# WASM SOURCE URLS
# =============================================================================
# Set these as secrets in the Cloudflare dashboard or via wrangler:
#
# PYMUPDF_SOURCE: Base URL to PyMuPDF WASM files
# Example: https://cdn.jsdelivr.net/npm/@bentopdf/pymupdf-wasm/assets
# https://your-bucket.r2.cloudflarestorage.com/pymupdf
#
# GS_SOURCE: Base URL to Ghostscript WASM files
# Example: https://cdn.jsdelivr.net/npm/@bentopdf/gs-wasm/assets
# https://your-bucket.r2.cloudflarestorage.com/gs
#
# CPDF_SOURCE: Base URL to CoherentPDF files
# Example: https://cdn.jsdelivr.net/npm/coherentpdf/cpdf
# https://your-bucket.r2.cloudflarestorage.com/cpdf
# =============================================================================
# USAGE FROM BENTOPDF
# =============================================================================
# In BentoPDF's WASM Settings page, configure URLs like:
# PyMuPDF: https://wasm.bentopdf.com/pymupdf/
# Ghostscript: https://wasm.bentopdf.com/gs/
# CoherentPDF: https://wasm.bentopdf.com/cpdf/
# =============================================================================
# RATE LIMITING (Optional but recommended)
# =============================================================================
# Create KV namespace:
# npx wrangler kv namespace create "RATE_LIMIT_KV"
#
# Then uncomment and update the ID below:
# [[kv_namespaces]]
# binding = "RATE_LIMIT_KV"
# id = "<YOUR_KV_NAMESPACE_ID>"
# Use the same KV namespace as the CORS proxy if you want shared rate limiting
[[kv_namespaces]]
binding = "RATE_LIMIT_KV"
id = "b88e030b308941118cd484e3fcb3ae49"
# =============================================================================
# CUSTOM DOMAIN (Optional)
# =============================================================================
# If you want a custom domain like wasm.bentopdf.com:
# routes = [
# { pattern = "wasm.bentopdf.com/*", zone_name = "bentopdf.com" }
# ]
-45
View File
@@ -1,45 +0,0 @@
name = "bentopdf-cors-proxy"
main = "cors-proxy-worker.js"
compatibility_date = "2024-01-01"
# Deploy to Cloudflare's global network
# If you are self-hosting, change the name to your worker name
# Run: npx wrangler deploy
# =============================================================================
# SECURITY FEATURES
# =============================================================================
#
# 1. SIGNATURE VERIFICATION (Optional - for anti-spoofing)
# - Generate secret: openssl rand -hex 32
# - Set secret: npx wrangler secret put PROXY_SECRET
# - Note: Secret is visible in frontend JS, so provides limited protection
#
# 2. RATE LIMITING (Recommended - requires KV)
# - Create KV namespace: npx wrangler kv namespace create "RATE_LIMIT_KV"
# - Uncomment the kv_namespaces section below with the returned ID
# - Limits: 60 requests per IP per minute
#
# 3. FILE SIZE LIMIT
# - Automatic: Rejects files larger than 1MB
# - Certificates are typically <10KB, so this prevents abuse
#
# 4. URL RESTRICTIONS
# - Only certificate URLs allowed (*.crt, *.cer, *.pem, /certs/, etc.)
# - Blocks private IPs (localhost, 10.x, 192.168.x, 172.16-31.x)
# =============================================================================
# KV NAMESPACE FOR RATE LIMITING
# =============================================================================
[[kv_namespaces]]
binding = "RATE_LIMIT_KV"
id = "b88e030b308941118cd484e3fcb3ae49"
# Optional: Custom domain routing
# routes = [
# { pattern = "cors-proxy.bentopdf.com/*", zone_name = "bentopdf.com" }
# ]
# Optional: Environment variables (for non-secret config)
# [vars]
# ALLOWED_ORIGINS = "https://www.bentopdf.com,https://bentopdf.com"
-169
View File
@@ -1,169 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- Primary Meta Tags -->
<title>Contact BentoPDF - Get in Touch | Support</title>
<meta
name="description"
content="Contact BentoPDF for support, questions, or feedback. Reach us at [email protected] - we're here to help with your PDF needs."
/>
<meta name="author" content="BentoPDF" />
<meta name="robots" content="index, follow" />
<!-- Canonical URL -->
<link rel="canonical" href="https://www.bentopdf.com/contact" />
<!-- Open Graph -->
<meta property="og:type" content="website" />
<meta property="og:url" content="https://www.bentopdf.com/contact" />
<meta
property="og:title"
content="Contact BentoPDF - Get in Touch | Support"
/>
<meta
property="og:description"
content="Contact BentoPDF for support, questions, or feedback. Reach us at [email protected] - we're here to help with your PDF needs."
/>
<meta
property="og:image"
content="https://www.bentopdf.com/images/og-contact.png"
/>
<meta property="og:site_name" content="BentoPDF" />
<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image" />
<meta
name="twitter:title"
content="Contact BentoPDF - Get in Touch | Support"
/>
<meta
name="twitter:description"
content="Contact BentoPDF for support, questions, or feedback. Reach us at [email protected] - we're here to help with your PDF needs."
/>
<meta
name="twitter:image"
content="https://www.bentopdf.com/images/twitter-contact.png"
/>
<meta name="twitter:site" content="@BentoPDF" />
<link href="/src/css/styles.css" rel="stylesheet" />
<link href="/src/css/landing.css" rel="stylesheet" />
<!-- Web App Manifest -->
<link rel="manifest" href="/site.webmanifest" />
<!-- Favicons -->
<link rel="icon" type="image/svg+xml" href="/images/favicon.svg" />
<link
rel="icon"
type="image/png"
sizes="192x192"
href="/images/favicon-192x192.png"
/>
<link
rel="icon"
type="image/png"
sizes="512x512"
href="/images/favicon-512x512.png"
/>
<link
rel="apple-touch-icon"
sizes="180x180"
href="/images/apple-touch-icon.png"
/>
<link rel="icon" href="/favicon.ico" sizes="32x32" />
</head>
<body class="lp antialiased">
{{> navbar }}
<div id="app">
<section id="contact-hero" class="lp-sec lp-sec--first">
<div class="lp-frame">
<div class="lp-cta">
<div>
<h1 class="lp-h2">
<span class="line" data-i18n="contact.title">Get in Touch</span>
</h1>
<p class="lp-head-p" data-i18n="contact.subtitle">
We'd love to hear from you. Whether you have a question,
feedback, or a feature request, please don't hesitate to reach
out.
</p>
</div>
</div>
</div>
</section>
<section id="contact-channels" class="lp-sec lp-band">
<div class="lp-frame">
<div class="lp-grid lp-grid--2 lp-cross">
<a class="lp-cell lp-reveal" href="mailto:[email protected]">
<span class="lp-tile"><i data-lucide="mail"></i></span>
<h3 data-i18n="contact.emailTitle">Email</h3>
<p data-i18n="contact.email">
You can reach us directly by email at:
</p>
<b>[email protected]</b>
</a>
<a
class="lp-cell lp-reveal"
href="https://github.com/alam00000/bentopdf/issues"
target="_blank"
rel="noopener noreferrer"
>
<span class="lp-tile"><i data-lucide="github"></i></span>
<h3>GitHub</h3>
<p data-i18n="contact.githubText">
Report a bug, request a feature, or read the source.
</p>
<b>github.com/alam00000/bentopdf</b>
</a>
<a
class="lp-cell lp-reveal"
href="https://discord.gg/Bgq3Ay3f2w"
target="_blank"
rel="noopener noreferrer"
>
<span class="lp-tile"><i data-lucide="message-circle"></i></span>
<h3>Discord</h3>
<p data-i18n="contact.discordText">
Join the community to chat with us and other users.
</p>
<b>discord.gg/Bgq3Ay3f2w</b>
</a>
<a class="lp-cell lp-reveal" href="faq.html">
<span class="lp-tile"><i data-lucide="help-circle"></i></span>
<h3 data-i18n="footer.faqLink">FAQ</h3>
<p data-i18n="contact.faqText">
Find quick answers to the questions we get most often.
</p>
<b data-i18n="contact.faqLink">Read the FAQ</b>
</a>
</div>
</div>
</section>
</div>
{{> footer }}
<script type="module" src="src/js/utils/lucide-init.ts"></script>
<script type="module" src="src/js/main.ts"></script>
<script type="module" src="src/js/mobileMenu.ts"></script>
<script type="module" src="src/js/landing.ts"></script>
<!-- JSON-LD Schema -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "ContactPage",
"name": "Contact BentoPDF - Get in Touch | Support",
"url": "https://www.bentopdf.com/contact"
}
</script>
</body>
</html>
-14
View File
@@ -1,14 +0,0 @@
services:
bentopdf:
build:
context: .
dockerfile: Dockerfile
args:
SIMPLE_MODE: false # false for default mode, true for simple mode
container_name: bentopdf
ports:
- '8080:8080'
restart: unless-stopped
# For IPv4-only environments
#environment:
# - DISABLE_IPV6=true
-16
View File
@@ -1,16 +0,0 @@
services:
bentopdf:
# GitHub Container Registry (Recommended)
# Self-Hosted build - ghcr.io/alam00000/bentopdf-simple:latest
# Commercial build - ghcr.io/alam00000/bentopdf:latest
# Docker Hub (Alternative)
# Self-Hosted build - bentopdfteam/bentopdf-simple:latest
# Commercial build - bentopdfteam/bentopdf:latest
image: ghcr.io/alam00000/bentopdf-simple:latest
container_name: bentopdf
restart: unless-stopped
ports:
- '8080:8080'
# For IPv4-only environments
#environment:
# - DISABLE_IPV6=true
+7
View File
@@ -0,0 +1,7 @@
{
"schemaVersion": 1,
"label": "Docker Downloads",
"message": "15.1M",
"color": "blue",
"namedLogo": "docker"
}
-283
View File
@@ -1,283 +0,0 @@
import { useMediaQuery } from './chunk-BRNHR3LR.js';
import { computed, ref, shallowRef, watch } from './chunk-H6MPEGKE.js';
// node_modules/vitepress/dist/client/theme-default/index.js
import '/Users/abdullahtapadar/Desktop/bento-pdf/node_modules/vitepress/dist/client/theme-default/styles/fonts.css';
// node_modules/vitepress/dist/client/theme-default/without-fonts.js
import '/Users/abdullahtapadar/Desktop/bento-pdf/node_modules/vitepress/dist/client/theme-default/styles/vars.css';
import '/Users/abdullahtapadar/Desktop/bento-pdf/node_modules/vitepress/dist/client/theme-default/styles/base.css';
import '/Users/abdullahtapadar/Desktop/bento-pdf/node_modules/vitepress/dist/client/theme-default/styles/icons.css';
import '/Users/abdullahtapadar/Desktop/bento-pdf/node_modules/vitepress/dist/client/theme-default/styles/utils.css';
import '/Users/abdullahtapadar/Desktop/bento-pdf/node_modules/vitepress/dist/client/theme-default/styles/components/custom-block.css';
import '/Users/abdullahtapadar/Desktop/bento-pdf/node_modules/vitepress/dist/client/theme-default/styles/components/vp-code.css';
import '/Users/abdullahtapadar/Desktop/bento-pdf/node_modules/vitepress/dist/client/theme-default/styles/components/vp-code-group.css';
import '/Users/abdullahtapadar/Desktop/bento-pdf/node_modules/vitepress/dist/client/theme-default/styles/components/vp-doc.css';
import '/Users/abdullahtapadar/Desktop/bento-pdf/node_modules/vitepress/dist/client/theme-default/styles/components/vp-sponsor.css';
import VPBadge from '/Users/abdullahtapadar/Desktop/bento-pdf/node_modules/vitepress/dist/client/theme-default/components/VPBadge.vue';
import Layout from '/Users/abdullahtapadar/Desktop/bento-pdf/node_modules/vitepress/dist/client/theme-default/Layout.vue';
import { default as default2 } from '/Users/abdullahtapadar/Desktop/bento-pdf/node_modules/vitepress/dist/client/theme-default/components/VPBadge.vue';
import { default as default3 } from '/Users/abdullahtapadar/Desktop/bento-pdf/node_modules/vitepress/dist/client/theme-default/components/VPButton.vue';
import { default as default4 } from '/Users/abdullahtapadar/Desktop/bento-pdf/node_modules/vitepress/dist/client/theme-default/components/VPDocAsideSponsors.vue';
import { default as default5 } from '/Users/abdullahtapadar/Desktop/bento-pdf/node_modules/vitepress/dist/client/theme-default/components/VPFeatures.vue';
import { default as default6 } from '/Users/abdullahtapadar/Desktop/bento-pdf/node_modules/vitepress/dist/client/theme-default/components/VPHomeContent.vue';
import { default as default7 } from '/Users/abdullahtapadar/Desktop/bento-pdf/node_modules/vitepress/dist/client/theme-default/components/VPHomeFeatures.vue';
import { default as default8 } from '/Users/abdullahtapadar/Desktop/bento-pdf/node_modules/vitepress/dist/client/theme-default/components/VPHomeHero.vue';
import { default as default9 } from '/Users/abdullahtapadar/Desktop/bento-pdf/node_modules/vitepress/dist/client/theme-default/components/VPHomeSponsors.vue';
import { default as default10 } from '/Users/abdullahtapadar/Desktop/bento-pdf/node_modules/vitepress/dist/client/theme-default/components/VPImage.vue';
import { default as default11 } from '/Users/abdullahtapadar/Desktop/bento-pdf/node_modules/vitepress/dist/client/theme-default/components/VPLink.vue';
import { default as default12 } from '/Users/abdullahtapadar/Desktop/bento-pdf/node_modules/vitepress/dist/client/theme-default/components/VPNavBarSearch.vue';
import { default as default13 } from '/Users/abdullahtapadar/Desktop/bento-pdf/node_modules/vitepress/dist/client/theme-default/components/VPSocialLink.vue';
import { default as default14 } from '/Users/abdullahtapadar/Desktop/bento-pdf/node_modules/vitepress/dist/client/theme-default/components/VPSocialLinks.vue';
import { default as default15 } from '/Users/abdullahtapadar/Desktop/bento-pdf/node_modules/vitepress/dist/client/theme-default/components/VPSponsors.vue';
import { default as default16 } from '/Users/abdullahtapadar/Desktop/bento-pdf/node_modules/vitepress/dist/client/theme-default/components/VPTeamMembers.vue';
import { default as default17 } from '/Users/abdullahtapadar/Desktop/bento-pdf/node_modules/vitepress/dist/client/theme-default/components/VPTeamPage.vue';
import { default as default18 } from '/Users/abdullahtapadar/Desktop/bento-pdf/node_modules/vitepress/dist/client/theme-default/components/VPTeamPageSection.vue';
import { default as default19 } from '/Users/abdullahtapadar/Desktop/bento-pdf/node_modules/vitepress/dist/client/theme-default/components/VPTeamPageTitle.vue';
// node_modules/vitepress/dist/client/theme-default/composables/local-nav.js
import { onContentUpdated } from 'vitepress';
// node_modules/vitepress/dist/client/theme-default/composables/outline.js
import { getScrollOffset } from 'vitepress';
// node_modules/vitepress/dist/client/theme-default/support/utils.js
import { withBase } from 'vitepress';
// node_modules/vitepress/dist/client/theme-default/composables/data.js
import { useData as useData$ } from 'vitepress';
var useData = useData$;
// node_modules/vitepress/dist/client/theme-default/support/utils.js
function ensureStartingSlash(path) {
return path.startsWith('/') ? path : `/${path}`;
}
// node_modules/vitepress/dist/client/theme-default/support/sidebar.js
function getSidebar(_sidebar, path) {
if (Array.isArray(_sidebar)) return addBase(_sidebar);
if (_sidebar == null) return [];
path = ensureStartingSlash(path);
const dir = Object.keys(_sidebar)
.sort((a, b) => {
return b.split('/').length - a.split('/').length;
})
.find((dir2) => {
return path.startsWith(ensureStartingSlash(dir2));
});
const sidebar = dir ? _sidebar[dir] : [];
return Array.isArray(sidebar)
? addBase(sidebar)
: addBase(sidebar.items, sidebar.base);
}
function getSidebarGroups(sidebar) {
const groups = [];
let lastGroupIndex = 0;
for (const index in sidebar) {
const item = sidebar[index];
if (item.items) {
lastGroupIndex = groups.push(item);
continue;
}
if (!groups[lastGroupIndex]) {
groups.push({ items: [] });
}
groups[lastGroupIndex].items.push(item);
}
return groups;
}
function addBase(items, _base) {
return [...items].map((_item) => {
const item = { ..._item };
const base = item.base || _base;
if (base && item.link) item.link = base + item.link;
if (item.items) item.items = addBase(item.items, base);
return item;
});
}
// node_modules/vitepress/dist/client/theme-default/composables/sidebar.js
function useSidebar() {
const { frontmatter, page, theme: theme2 } = useData();
const is960 = useMediaQuery('(min-width: 960px)');
const isOpen = ref(false);
const _sidebar = computed(() => {
const sidebarConfig = theme2.value.sidebar;
const relativePath = page.value.relativePath;
return sidebarConfig ? getSidebar(sidebarConfig, relativePath) : [];
});
const sidebar = ref(_sidebar.value);
watch(_sidebar, (next, prev) => {
if (JSON.stringify(next) !== JSON.stringify(prev))
sidebar.value = _sidebar.value;
});
const hasSidebar = computed(() => {
return (
frontmatter.value.sidebar !== false &&
sidebar.value.length > 0 &&
frontmatter.value.layout !== 'home'
);
});
const leftAside = computed(() => {
if (hasAside)
return frontmatter.value.aside == null
? theme2.value.aside === 'left'
: frontmatter.value.aside === 'left';
return false;
});
const hasAside = computed(() => {
if (frontmatter.value.layout === 'home') return false;
if (frontmatter.value.aside != null) return !!frontmatter.value.aside;
return theme2.value.aside !== false;
});
const isSidebarEnabled = computed(() => hasSidebar.value && is960.value);
const sidebarGroups = computed(() => {
return hasSidebar.value ? getSidebarGroups(sidebar.value) : [];
});
function open() {
isOpen.value = true;
}
function close() {
isOpen.value = false;
}
function toggle() {
isOpen.value ? close() : open();
}
return {
isOpen,
sidebar,
sidebarGroups,
hasSidebar,
hasAside,
leftAside,
isSidebarEnabled,
open,
close,
toggle,
};
}
// node_modules/vitepress/dist/client/theme-default/composables/outline.js
var ignoreRE = /\b(?:VPBadge|header-anchor|footnote-ref|ignore-header)\b/;
var resolvedHeaders = [];
function getHeaders(range) {
const headers = [
...document.querySelectorAll('.VPDoc :where(h1,h2,h3,h4,h5,h6)'),
]
.filter((el) => el.id && el.hasChildNodes())
.map((el) => {
const level = Number(el.tagName[1]);
return {
element: el,
title: serializeHeader(el),
link: '#' + el.id,
level,
};
});
return resolveHeaders(headers, range);
}
function serializeHeader(h) {
let ret = '';
for (const node of h.childNodes) {
if (node.nodeType === 1) {
if (ignoreRE.test(node.className)) continue;
ret += node.textContent;
} else if (node.nodeType === 3) {
ret += node.textContent;
}
}
return ret.trim();
}
function resolveHeaders(headers, range) {
if (range === false) {
return [];
}
const levelsRange =
(typeof range === 'object' && !Array.isArray(range)
? range.level
: range) || 2;
const [high, low] =
typeof levelsRange === 'number'
? [levelsRange, levelsRange]
: levelsRange === 'deep'
? [2, 6]
: levelsRange;
return buildTree(headers, high, low);
}
function buildTree(data, min, max) {
resolvedHeaders.length = 0;
const result = [];
const stack = [];
data.forEach((item) => {
const node = { ...item, children: [] };
let parent = stack[stack.length - 1];
while (parent && parent.level >= node.level) {
stack.pop();
parent = stack[stack.length - 1];
}
if (
node.element.classList.contains('ignore-header') ||
(parent && 'shouldIgnore' in parent)
) {
stack.push({ level: node.level, shouldIgnore: true });
return;
}
if (node.level > max || node.level < min) return;
resolvedHeaders.push({ element: node.element, link: node.link });
if (parent) parent.children.push(node);
else result.push(node);
stack.push(node);
});
return result;
}
// node_modules/vitepress/dist/client/theme-default/composables/local-nav.js
function useLocalNav() {
const { theme: theme2, frontmatter } = useData();
const headers = shallowRef([]);
const hasLocalNav = computed(() => {
return headers.value.length > 0;
});
onContentUpdated(() => {
headers.value = getHeaders(
frontmatter.value.outline ?? theme2.value.outline
);
});
return {
headers,
hasLocalNav,
};
}
// node_modules/vitepress/dist/client/theme-default/without-fonts.js
var theme = {
Layout,
enhanceApp: ({ app }) => {
app.component('Badge', VPBadge);
},
};
var without_fonts_default = theme;
export {
default2 as VPBadge,
default3 as VPButton,
default4 as VPDocAsideSponsors,
default5 as VPFeatures,
default6 as VPHomeContent,
default7 as VPHomeFeatures,
default8 as VPHomeHero,
default9 as VPHomeSponsors,
default10 as VPImage,
default11 as VPLink,
default12 as VPNavBarSearch,
default13 as VPSocialLink,
default14 as VPSocialLinks,
default15 as VPSponsors,
default16 as VPTeamMembers,
default17 as VPTeamPage,
default18 as VPTeamPageSection,
default19 as VPTeamPageTitle,
without_fonts_default as default,
useLocalNav,
useSidebar,
};
//# sourceMappingURL=@theme_index.js.map
File diff suppressed because one or more lines are too long
-58
View File
@@ -1,58 +0,0 @@
{
"hash": "cbe76ac1",
"configHash": "fcacb6fc",
"lockfileHash": "25b6b516",
"browserHash": "e3bcae4a",
"optimized": {
"vue": {
"src": "../../../../node_modules/vue/dist/vue.runtime.esm-bundler.js",
"file": "vue.js",
"fileHash": "d430f9d5",
"needsInterop": false
},
"vitepress > @vue/devtools-api": {
"src": "../../../../node_modules/@vue/devtools-api/dist/index.js",
"file": "vitepress___@vue_devtools-api.js",
"fileHash": "cdc3240f",
"needsInterop": false
},
"vitepress > @vueuse/core": {
"src": "../../../../node_modules/@vueuse/core/index.mjs",
"file": "vitepress___@vueuse_core.js",
"fileHash": "9c54fae4",
"needsInterop": false
},
"vitepress > @vueuse/integrations/useFocusTrap": {
"src": "../../../../node_modules/@vueuse/integrations/useFocusTrap.mjs",
"file": "vitepress___@vueuse_integrations_useFocusTrap.js",
"fileHash": "dcb2d016",
"needsInterop": false
},
"vitepress > mark.js/src/vanilla.js": {
"src": "../../../../node_modules/mark.js/src/vanilla.js",
"file": "vitepress___mark__js_src_vanilla__js.js",
"fileHash": "89ac005f",
"needsInterop": false
},
"vitepress > minisearch": {
"src": "../../../../node_modules/minisearch/dist/es/index.js",
"file": "vitepress___minisearch.js",
"fileHash": "dcc8fa95",
"needsInterop": false
},
"@theme/index": {
"src": "../../../../node_modules/vitepress/dist/client/theme-default/index.js",
"file": "@theme_index.js",
"fileHash": "12a406fc",
"needsInterop": false
}
},
"chunks": {
"chunk-BRNHR3LR": {
"file": "chunk-BRNHR3LR.js"
},
"chunk-H6MPEGKE": {
"file": "chunk-H6MPEGKE.js"
}
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
-3
View File
@@ -1,3 +0,0 @@
{
"type": "module"
}
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
-583
View File
@@ -1,583 +0,0 @@
import {
DefaultMagicKeysAliasMap,
StorageSerializers,
TransitionPresets,
assert,
breakpointsAntDesign,
breakpointsBootstrapV5,
breakpointsElement,
breakpointsMasterCss,
breakpointsPrimeFlex,
breakpointsQuasar,
breakpointsSematic,
breakpointsTailwind,
breakpointsVuetify,
breakpointsVuetifyV2,
breakpointsVuetifyV3,
bypassFilter,
camelize,
clamp,
cloneFnJSON,
computedAsync,
computedEager,
computedInject,
computedWithControl,
containsProp,
controlledRef,
createEventHook,
createFetch,
createFilterWrapper,
createGlobalState,
createInjectionState,
createRef,
createReusableTemplate,
createSharedComposable,
createSingletonPromise,
createTemplatePromise,
createUnrefFn,
customStorageEventName,
debounceFilter,
defaultDocument,
defaultLocation,
defaultNavigator,
defaultWindow,
executeTransition,
extendRef,
formatDate,
formatTimeAgo,
get,
getLifeCycleTarget,
getSSRHandler,
hasOwn,
hyphenate,
identity,
increaseWithUnit,
injectLocal,
invoke,
isClient,
isDef,
isDefined,
isIOS,
isObject,
isWorker,
makeDestructurable,
mapGamepadToXbox360Controller,
noop,
normalizeDate,
notNullish,
now,
objectEntries,
objectOmit,
objectPick,
onClickOutside,
onElementRemoval,
onKeyDown,
onKeyPressed,
onKeyStroke,
onKeyUp,
onLongPress,
onStartTyping,
pausableFilter,
promiseTimeout,
provideLocal,
provideSSRWidth,
pxValue,
rand,
reactify,
reactifyObject,
reactiveComputed,
reactiveOmit,
reactivePick,
refAutoReset,
refDebounced,
refDefault,
refThrottled,
refWithControl,
resolveRef,
resolveUnref,
set,
setSSRHandler,
syncRef,
syncRefs,
templateRef,
throttleFilter,
timestamp,
toArray,
toReactive,
toRef,
toRefs,
toValue,
tryOnBeforeMount,
tryOnBeforeUnmount,
tryOnMounted,
tryOnScopeDispose,
tryOnUnmounted,
unrefElement,
until,
useActiveElement,
useAnimate,
useArrayDifference,
useArrayEvery,
useArrayFilter,
useArrayFind,
useArrayFindIndex,
useArrayFindLast,
useArrayIncludes,
useArrayJoin,
useArrayMap,
useArrayReduce,
useArraySome,
useArrayUnique,
useAsyncQueue,
useAsyncState,
useBase64,
useBattery,
useBluetooth,
useBreakpoints,
useBroadcastChannel,
useBrowserLocation,
useCached,
useClipboard,
useClipboardItems,
useCloned,
useColorMode,
useConfirmDialog,
useCountdown,
useCounter,
useCssVar,
useCurrentElement,
useCycleList,
useDark,
useDateFormat,
useDebounceFn,
useDebouncedRefHistory,
useDeviceMotion,
useDeviceOrientation,
useDevicePixelRatio,
useDevicesList,
useDisplayMedia,
useDocumentVisibility,
useDraggable,
useDropZone,
useElementBounding,
useElementByPoint,
useElementHover,
useElementSize,
useElementVisibility,
useEventBus,
useEventListener,
useEventSource,
useEyeDropper,
useFavicon,
useFetch,
useFileDialog,
useFileSystemAccess,
useFocus,
useFocusWithin,
useFps,
useFullscreen,
useGamepad,
useGeolocation,
useIdle,
useImage,
useInfiniteScroll,
useIntersectionObserver,
useInterval,
useIntervalFn,
useKeyModifier,
useLastChanged,
useLocalStorage,
useMagicKeys,
useManualRefHistory,
useMediaControls,
useMediaQuery,
useMemoize,
useMemory,
useMounted,
useMouse,
useMouseInElement,
useMousePressed,
useMutationObserver,
useNavigatorLanguage,
useNetwork,
useNow,
useObjectUrl,
useOffsetPagination,
useOnline,
usePageLeave,
useParallax,
useParentElement,
usePerformanceObserver,
usePermission,
usePointer,
usePointerLock,
usePointerSwipe,
usePreferredColorScheme,
usePreferredContrast,
usePreferredDark,
usePreferredLanguages,
usePreferredReducedMotion,
usePreferredReducedTransparency,
usePrevious,
useRafFn,
useRefHistory,
useResizeObserver,
useSSRWidth,
useScreenOrientation,
useScreenSafeArea,
useScriptTag,
useScroll,
useScrollLock,
useSessionStorage,
useShare,
useSorted,
useSpeechRecognition,
useSpeechSynthesis,
useStepper,
useStorage,
useStorageAsync,
useStyleTag,
useSupported,
useSwipe,
useTemplateRefsList,
useTextDirection,
useTextSelection,
useTextareaAutosize,
useThrottleFn,
useThrottledRefHistory,
useTimeAgo,
useTimeout,
useTimeoutFn,
useTimeoutPoll,
useTimestamp,
useTitle,
useToNumber,
useToString,
useToggle,
useTransition,
useUrlSearchParams,
useUserMedia,
useVModel,
useVModels,
useVibrate,
useVirtualList,
useWakeLock,
useWebNotification,
useWebSocket,
useWebWorker,
useWebWorkerFn,
useWindowFocus,
useWindowScroll,
useWindowSize,
watchArray,
watchAtMost,
watchDebounced,
watchDeep,
watchIgnorable,
watchImmediate,
watchOnce,
watchPausable,
watchThrottled,
watchTriggerable,
watchWithFilter,
whenever,
} from './chunk-BRNHR3LR.js';
import './chunk-H6MPEGKE.js';
export {
DefaultMagicKeysAliasMap,
StorageSerializers,
TransitionPresets,
assert,
computedAsync as asyncComputed,
refAutoReset as autoResetRef,
breakpointsAntDesign,
breakpointsBootstrapV5,
breakpointsElement,
breakpointsMasterCss,
breakpointsPrimeFlex,
breakpointsQuasar,
breakpointsSematic,
breakpointsTailwind,
breakpointsVuetify,
breakpointsVuetifyV2,
breakpointsVuetifyV3,
bypassFilter,
camelize,
clamp,
cloneFnJSON,
computedAsync,
computedEager,
computedInject,
computedWithControl,
containsProp,
computedWithControl as controlledComputed,
controlledRef,
createEventHook,
createFetch,
createFilterWrapper,
createGlobalState,
createInjectionState,
reactify as createReactiveFn,
createRef,
createReusableTemplate,
createSharedComposable,
createSingletonPromise,
createTemplatePromise,
createUnrefFn,
customStorageEventName,
debounceFilter,
refDebounced as debouncedRef,
watchDebounced as debouncedWatch,
defaultDocument,
defaultLocation,
defaultNavigator,
defaultWindow,
computedEager as eagerComputed,
executeTransition,
extendRef,
formatDate,
formatTimeAgo,
get,
getLifeCycleTarget,
getSSRHandler,
hasOwn,
hyphenate,
identity,
watchIgnorable as ignorableWatch,
increaseWithUnit,
injectLocal,
invoke,
isClient,
isDef,
isDefined,
isIOS,
isObject,
isWorker,
makeDestructurable,
mapGamepadToXbox360Controller,
noop,
normalizeDate,
notNullish,
now,
objectEntries,
objectOmit,
objectPick,
onClickOutside,
onElementRemoval,
onKeyDown,
onKeyPressed,
onKeyStroke,
onKeyUp,
onLongPress,
onStartTyping,
pausableFilter,
watchPausable as pausableWatch,
promiseTimeout,
provideLocal,
provideSSRWidth,
pxValue,
rand,
reactify,
reactifyObject,
reactiveComputed,
reactiveOmit,
reactivePick,
refAutoReset,
refDebounced,
refDefault,
refThrottled,
refWithControl,
resolveRef,
resolveUnref,
set,
setSSRHandler,
syncRef,
syncRefs,
templateRef,
throttleFilter,
refThrottled as throttledRef,
watchThrottled as throttledWatch,
timestamp,
toArray,
toReactive,
toRef,
toRefs,
toValue,
tryOnBeforeMount,
tryOnBeforeUnmount,
tryOnMounted,
tryOnScopeDispose,
tryOnUnmounted,
unrefElement,
until,
useActiveElement,
useAnimate,
useArrayDifference,
useArrayEvery,
useArrayFilter,
useArrayFind,
useArrayFindIndex,
useArrayFindLast,
useArrayIncludes,
useArrayJoin,
useArrayMap,
useArrayReduce,
useArraySome,
useArrayUnique,
useAsyncQueue,
useAsyncState,
useBase64,
useBattery,
useBluetooth,
useBreakpoints,
useBroadcastChannel,
useBrowserLocation,
useCached,
useClipboard,
useClipboardItems,
useCloned,
useColorMode,
useConfirmDialog,
useCountdown,
useCounter,
useCssVar,
useCurrentElement,
useCycleList,
useDark,
useDateFormat,
refDebounced as useDebounce,
useDebounceFn,
useDebouncedRefHistory,
useDeviceMotion,
useDeviceOrientation,
useDevicePixelRatio,
useDevicesList,
useDisplayMedia,
useDocumentVisibility,
useDraggable,
useDropZone,
useElementBounding,
useElementByPoint,
useElementHover,
useElementSize,
useElementVisibility,
useEventBus,
useEventListener,
useEventSource,
useEyeDropper,
useFavicon,
useFetch,
useFileDialog,
useFileSystemAccess,
useFocus,
useFocusWithin,
useFps,
useFullscreen,
useGamepad,
useGeolocation,
useIdle,
useImage,
useInfiniteScroll,
useIntersectionObserver,
useInterval,
useIntervalFn,
useKeyModifier,
useLastChanged,
useLocalStorage,
useMagicKeys,
useManualRefHistory,
useMediaControls,
useMediaQuery,
useMemoize,
useMemory,
useMounted,
useMouse,
useMouseInElement,
useMousePressed,
useMutationObserver,
useNavigatorLanguage,
useNetwork,
useNow,
useObjectUrl,
useOffsetPagination,
useOnline,
usePageLeave,
useParallax,
useParentElement,
usePerformanceObserver,
usePermission,
usePointer,
usePointerLock,
usePointerSwipe,
usePreferredColorScheme,
usePreferredContrast,
usePreferredDark,
usePreferredLanguages,
usePreferredReducedMotion,
usePreferredReducedTransparency,
usePrevious,
useRafFn,
useRefHistory,
useResizeObserver,
useSSRWidth,
useScreenOrientation,
useScreenSafeArea,
useScriptTag,
useScroll,
useScrollLock,
useSessionStorage,
useShare,
useSorted,
useSpeechRecognition,
useSpeechSynthesis,
useStepper,
useStorage,
useStorageAsync,
useStyleTag,
useSupported,
useSwipe,
useTemplateRefsList,
useTextDirection,
useTextSelection,
useTextareaAutosize,
refThrottled as useThrottle,
useThrottleFn,
useThrottledRefHistory,
useTimeAgo,
useTimeout,
useTimeoutFn,
useTimeoutPoll,
useTimestamp,
useTitle,
useToNumber,
useToString,
useToggle,
useTransition,
useUrlSearchParams,
useUserMedia,
useVModel,
useVModels,
useVibrate,
useVirtualList,
useWakeLock,
useWebNotification,
useWebSocket,
useWebWorker,
useWebWorkerFn,
useWindowFocus,
useWindowScroll,
useWindowSize,
watchArray,
watchAtMost,
watchDebounced,
watchDeep,
watchIgnorable,
watchImmediate,
watchOnce,
watchPausable,
watchThrottled,
watchTriggerable,
watchWithFilter,
whenever,
};
//# sourceMappingURL=vitepress___@vueuse_core.js.map
@@ -1,7 +0,0 @@
{
"version": 3,
"sources": [],
"sourcesContent": [],
"mappings": "",
"names": []
}
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
-347
View File
@@ -1,347 +0,0 @@
import {
BaseTransition,
BaseTransitionPropsValidators,
Comment,
DeprecationTypes,
EffectScope,
ErrorCodes,
ErrorTypeStrings,
Fragment,
KeepAlive,
ReactiveEffect,
Static,
Suspense,
Teleport,
Text,
TrackOpTypes,
Transition,
TransitionGroup,
TriggerOpTypes,
VueElement,
assertNumber,
callWithAsyncErrorHandling,
callWithErrorHandling,
camelize,
capitalize,
cloneVNode,
compatUtils,
compile,
computed,
createApp,
createBaseVNode,
createBlock,
createCommentVNode,
createElementBlock,
createHydrationRenderer,
createPropsRestProxy,
createRenderer,
createSSRApp,
createSlots,
createStaticVNode,
createTextVNode,
createVNode,
customRef,
defineAsyncComponent,
defineComponent,
defineCustomElement,
defineEmits,
defineExpose,
defineModel,
defineOptions,
defineProps,
defineSSRCustomElement,
defineSlots,
devtools,
effect,
effectScope,
getCurrentInstance,
getCurrentScope,
getCurrentWatcher,
getTransitionRawChildren,
guardReactiveProps,
h,
handleError,
hasInjectionContext,
hydrate,
hydrateOnIdle,
hydrateOnInteraction,
hydrateOnMediaQuery,
hydrateOnVisible,
initCustomFormatter,
initDirectivesForSSR,
inject,
isMemoSame,
isProxy,
isReactive,
isReadonly,
isRef,
isRuntimeOnly,
isShallow,
isVNode,
markRaw,
mergeDefaults,
mergeModels,
mergeProps,
nextTick,
nodeOps,
normalizeClass,
normalizeProps,
normalizeStyle,
onActivated,
onBeforeMount,
onBeforeUnmount,
onBeforeUpdate,
onDeactivated,
onErrorCaptured,
onMounted,
onRenderTracked,
onRenderTriggered,
onScopeDispose,
onServerPrefetch,
onUnmounted,
onUpdated,
onWatcherCleanup,
openBlock,
patchProp,
popScopeId,
provide,
proxyRefs,
pushScopeId,
queuePostFlushCb,
reactive,
readonly,
ref,
registerRuntimeCompiler,
render,
renderList,
renderSlot,
resolveComponent,
resolveDirective,
resolveDynamicComponent,
resolveFilter,
resolveTransitionHooks,
setBlockTracking,
setDevtoolsHook,
setTransitionHooks,
shallowReactive,
shallowReadonly,
shallowRef,
ssrContextKey,
ssrUtils,
stop,
toDisplayString,
toHandlerKey,
toHandlers,
toRaw,
toRef,
toRefs,
toValue,
transformVNodeArgs,
triggerRef,
unref,
useAttrs,
useCssModule,
useCssVars,
useHost,
useId,
useModel,
useSSRContext,
useShadowRoot,
useSlots,
useTemplateRef,
useTransitionState,
vModelCheckbox,
vModelDynamic,
vModelRadio,
vModelSelect,
vModelText,
vShow,
version,
warn,
watch,
watchEffect,
watchPostEffect,
watchSyncEffect,
withAsyncContext,
withCtx,
withDefaults,
withDirectives,
withKeys,
withMemo,
withModifiers,
withScopeId,
} from './chunk-H6MPEGKE.js';
export {
BaseTransition,
BaseTransitionPropsValidators,
Comment,
DeprecationTypes,
EffectScope,
ErrorCodes,
ErrorTypeStrings,
Fragment,
KeepAlive,
ReactiveEffect,
Static,
Suspense,
Teleport,
Text,
TrackOpTypes,
Transition,
TransitionGroup,
TriggerOpTypes,
VueElement,
assertNumber,
callWithAsyncErrorHandling,
callWithErrorHandling,
camelize,
capitalize,
cloneVNode,
compatUtils,
compile,
computed,
createApp,
createBlock,
createCommentVNode,
createElementBlock,
createBaseVNode as createElementVNode,
createHydrationRenderer,
createPropsRestProxy,
createRenderer,
createSSRApp,
createSlots,
createStaticVNode,
createTextVNode,
createVNode,
customRef,
defineAsyncComponent,
defineComponent,
defineCustomElement,
defineEmits,
defineExpose,
defineModel,
defineOptions,
defineProps,
defineSSRCustomElement,
defineSlots,
devtools,
effect,
effectScope,
getCurrentInstance,
getCurrentScope,
getCurrentWatcher,
getTransitionRawChildren,
guardReactiveProps,
h,
handleError,
hasInjectionContext,
hydrate,
hydrateOnIdle,
hydrateOnInteraction,
hydrateOnMediaQuery,
hydrateOnVisible,
initCustomFormatter,
initDirectivesForSSR,
inject,
isMemoSame,
isProxy,
isReactive,
isReadonly,
isRef,
isRuntimeOnly,
isShallow,
isVNode,
markRaw,
mergeDefaults,
mergeModels,
mergeProps,
nextTick,
nodeOps,
normalizeClass,
normalizeProps,
normalizeStyle,
onActivated,
onBeforeMount,
onBeforeUnmount,
onBeforeUpdate,
onDeactivated,
onErrorCaptured,
onMounted,
onRenderTracked,
onRenderTriggered,
onScopeDispose,
onServerPrefetch,
onUnmounted,
onUpdated,
onWatcherCleanup,
openBlock,
patchProp,
popScopeId,
provide,
proxyRefs,
pushScopeId,
queuePostFlushCb,
reactive,
readonly,
ref,
registerRuntimeCompiler,
render,
renderList,
renderSlot,
resolveComponent,
resolveDirective,
resolveDynamicComponent,
resolveFilter,
resolveTransitionHooks,
setBlockTracking,
setDevtoolsHook,
setTransitionHooks,
shallowReactive,
shallowReadonly,
shallowRef,
ssrContextKey,
ssrUtils,
stop,
toDisplayString,
toHandlerKey,
toHandlers,
toRaw,
toRef,
toRefs,
toValue,
transformVNodeArgs,
triggerRef,
unref,
useAttrs,
useCssModule,
useCssVars,
useHost,
useId,
useModel,
useSSRContext,
useShadowRoot,
useSlots,
useTemplateRef,
useTransitionState,
vModelCheckbox,
vModelDynamic,
vModelRadio,
vModelSelect,
vModelText,
vShow,
version,
warn,
watch,
watchEffect,
watchPostEffect,
watchSyncEffect,
withAsyncContext,
withCtx,
withDefaults,
withDirectives,
withKeys,
withMemo,
withModifiers,
withScopeId,
};
//# sourceMappingURL=vue.js.map
-7
View File
@@ -1,7 +0,0 @@
{
"version": 3,
"sources": [],
"sourcesContent": [],
"mappings": "",
"names": []
}
-234
View File
@@ -1,234 +0,0 @@
import { defineConfig } from 'vitepress'
const SITE_URL = 'https://www.bentopdf.com'
export default defineConfig({
title: "BentoPDF Docs",
description: "Documentation for BentoPDF - The free, open-source, privacy-first PDF toolkit",
base: '/docs/',
cleanUrls: true,
transformPageData(pageData) {
const relPath = pageData.relativePath.replace(/\.md$/, '')
const slug = relPath === 'index' ? '' : relPath.replace(/\/index$/, '/')
const canonicalUrl = slug ? `${SITE_URL}/docs/${slug}` : `${SITE_URL}/docs/`
pageData.frontmatter.head ??= []
pageData.frontmatter.head.push(['link', { rel: 'canonical', href: canonicalUrl }])
},
themeConfig: {
logo: '/images/favicon-no-bg.svg',
nav: [
{ text: 'Home', link: '/' },
{ text: 'Getting Started', link: '/getting-started' },
{ text: 'Tools', link: '/tools/' },
{ text: 'Self-Hosting', link: '/self-hosting/' },
{ text: 'Contributing', link: '/contributing' },
{ text: 'Commercial License', link: '/licensing' }
],
sidebar: {
'/tools/': [
{
text: 'Tools Reference',
items: [
{ text: 'Overview', link: '/tools/' }
]
},
{
text: 'Edit & Annotate',
collapsed: false,
items: [
{ text: 'PDF Editor', link: '/tools/edit-pdf' },
{ text: 'Edit Bookmarks', link: '/tools/bookmark' },
{ text: 'Table of Contents', link: '/tools/table-of-contents' },
{ text: 'Page Numbers', link: '/tools/page-numbers' },
{ text: 'Add Page Labels', link: '/tools/add-page-labels' },
{ text: 'Bates Numbering', link: '/tools/bates-numbering' },
{ text: 'Add Watermark', link: '/tools/add-watermark' },
{ text: 'Header & Footer', link: '/tools/header-footer' },
{ text: 'Invert Colors', link: '/tools/invert-colors' },
{ text: 'Scanner Effect', link: '/tools/scanner-effect' },
{ text: 'Adjust Colors', link: '/tools/adjust-colors' },
{ text: 'Background Color', link: '/tools/background-color' },
{ text: 'Change Text Color', link: '/tools/text-color' },
{ text: 'Sign PDF', link: '/tools/sign-pdf' },
{ text: 'Add Stamps', link: '/tools/add-stamps' },
{ text: 'Remove Annotations', link: '/tools/remove-annotations' },
{ text: 'Crop PDF', link: '/tools/crop-pdf' },
{ text: 'PDF Form Filler', link: '/tools/form-filler' },
{ text: 'Create PDF Form', link: '/tools/form-creator' },
{ text: 'Remove Blank Pages', link: '/tools/remove-blank-pages' }
]
},
{
text: 'Convert to PDF',
collapsed: true,
items: [
{ text: 'Images to PDF', link: '/tools/image-to-pdf' },
{ text: 'JPG to PDF', link: '/tools/jpg-to-pdf' },
{ text: 'PNG to PDF', link: '/tools/png-to-pdf' },
{ text: 'WebP to PDF', link: '/tools/webp-to-pdf' },
{ text: 'SVG to PDF', link: '/tools/svg-to-pdf' },
{ text: 'BMP to PDF', link: '/tools/bmp-to-pdf' },
{ text: 'HEIC to PDF', link: '/tools/heic-to-pdf' },
{ text: 'TIFF to PDF', link: '/tools/tiff-to-pdf' },
{ text: 'Text to PDF', link: '/tools/txt-to-pdf' },
{ text: 'Markdown to PDF', link: '/tools/markdown-to-pdf' },
{ text: 'JSON to PDF', link: '/tools/json-to-pdf' },
{ text: 'ODT to PDF', link: '/tools/odt-to-pdf' },
{ text: 'CSV to PDF', link: '/tools/csv-to-pdf' },
{ text: 'RTF to PDF', link: '/tools/rtf-to-pdf' },
{ text: 'Word to PDF', link: '/tools/word-to-pdf' },
{ text: 'Excel to PDF', link: '/tools/excel-to-pdf' },
{ text: 'PowerPoint to PDF', link: '/tools/powerpoint-to-pdf' },
{ text: 'XPS to PDF', link: '/tools/xps-to-pdf' },
{ text: 'MOBI to PDF', link: '/tools/mobi-to-pdf' },
{ text: 'EPUB to PDF', link: '/tools/epub-to-pdf' },
{ text: 'FB2 to PDF', link: '/tools/fb2-to-pdf' },
{ text: 'CBZ to PDF', link: '/tools/cbz-to-pdf' },
{ text: 'WPD to PDF', link: '/tools/wpd-to-pdf' },
{ text: 'WPS to PDF', link: '/tools/wps-to-pdf' },
{ text: 'XML to PDF', link: '/tools/xml-to-pdf' },
{ text: 'ODG to PDF', link: '/tools/odg-to-pdf' },
{ text: 'ODS to PDF', link: '/tools/ods-to-pdf' },
{ text: 'ODP to PDF', link: '/tools/odp-to-pdf' },
{ text: 'PUB to PDF', link: '/tools/pub-to-pdf' },
{ text: 'VSD to PDF', link: '/tools/vsd-to-pdf' },
{ text: 'PSD to PDF', link: '/tools/psd-to-pdf' },
{ text: 'Email to PDF', link: '/tools/email-to-pdf' }
]
},
{
text: 'Convert from PDF',
collapsed: true,
items: [
{ text: 'PDF to JPG', link: '/tools/pdf-to-jpg' },
{ text: 'PDF to PNG', link: '/tools/pdf-to-png' },
{ text: 'PDF to WebP', link: '/tools/pdf-to-webp' },
{ text: 'PDF to BMP', link: '/tools/pdf-to-bmp' },
{ text: 'PDF to TIFF', link: '/tools/pdf-to-tiff' },
{ text: 'PDF to CBZ', link: '/tools/pdf-to-cbz' },
{ text: 'PDF to SVG', link: '/tools/pdf-to-svg' },
{ text: 'PDF to CSV', link: '/tools/pdf-to-csv' },
{ text: 'PDF to Excel', link: '/tools/pdf-to-excel' },
{ text: 'PDF to Greyscale', link: '/tools/pdf-to-greyscale' },
{ text: 'PDF to JSON', link: '/tools/pdf-to-json' },
{ text: 'PDF to Word', link: '/tools/pdf-to-word' },
{ text: 'Extract Images', link: '/tools/extract-images' },
{ text: 'PDF to Markdown', link: '/tools/pdf-to-markdown' },
{ text: 'Prepare PDF for AI', link: '/tools/prepare-pdf-for-ai' },
{ text: 'PDF to Text', link: '/tools/pdf-to-text' },
{ text: 'Extract Tables', link: '/tools/extract-tables' }
]
},
{
text: 'Organize & Manage',
collapsed: true,
items: [
{ text: 'PDF Workflow Builder', link: '/tools/pdf-workflow' },
{ text: 'PDF Multi Tool', link: '/tools/pdf-multi-tool' },
{ text: 'OCR PDF', link: '/tools/ocr-pdf' },
{ text: 'Merge PDF', link: '/tools/merge-pdf' },
{ text: 'Alternate & Mix Pages', link: '/tools/alternate-merge' },
{ text: 'Duplex Collate', link: '/tools/duplex-collate' },
{ text: 'Organize & Duplicate', link: '/tools/organize-pdf' },
{ text: 'Add Attachments', link: '/tools/add-attachments' },
{ text: 'Extract Attachments', link: '/tools/extract-attachments' },
{ text: 'Edit Attachments', link: '/tools/edit-attachments' },
{ text: 'PDF OCG', link: '/tools/pdf-layers' },
{ text: 'Split PDF', link: '/tools/split-pdf' },
{ text: 'Divide Pages', link: '/tools/divide-pages' },
{ text: 'Extract Pages', link: '/tools/extract-pages' },
{ text: 'Delete Pages', link: '/tools/delete-pages' },
{ text: 'Add Blank Page', link: '/tools/add-blank-page' },
{ text: 'Reverse Pages', link: '/tools/reverse-pages' },
{ text: 'Rotate PDF', link: '/tools/rotate-pdf' },
{ text: 'Rotate by Custom Degrees', link: '/tools/rotate-custom' },
{ text: 'N-Up PDF', link: '/tools/n-up-pdf' },
{ text: 'PDF Booklet', link: '/tools/pdf-booklet' },
{ text: 'Combine to Single Page', link: '/tools/combine-single-page' },
{ text: 'View Metadata', link: '/tools/view-metadata' },
{ text: 'Edit Metadata', link: '/tools/edit-metadata' },
{ text: 'PDFs to ZIP', link: '/tools/pdf-to-zip' },
{ text: 'Compare PDFs', link: '/tools/compare-pdfs' },
{ text: 'Posterize PDF', link: '/tools/posterize-pdf' }
]
},
{
text: 'Optimize & Repair',
collapsed: true,
items: [
{ text: 'Compress PDF', link: '/tools/compress-pdf' },
{ text: 'PDF to PDF/A', link: '/tools/pdf-to-pdfa' },
{ text: 'Fix Page Size', link: '/tools/fix-page-size' },
{ text: 'Linearize PDF', link: '/tools/linearize-pdf' },
{ text: 'Page Dimensions', link: '/tools/page-dimensions' },
{ text: 'Remove Restrictions', link: '/tools/remove-restrictions' },
{ text: 'Repair PDF', link: '/tools/repair-pdf' },
{ text: 'Rasterize PDF', link: '/tools/rasterize-pdf' },
{ text: 'Deskew PDF', link: '/tools/deskew-pdf' },
{ text: 'Font to Outline', link: '/tools/font-to-outline' }
]
},
{
text: 'Secure PDF',
collapsed: true,
items: [
{ text: 'Protect PDF', link: '/tools/protect-pdf' },
{ text: 'Sanitize PDF', link: '/tools/sanitize-pdf' },
{ text: 'Unlock PDF', link: '/tools/unlock-pdf' },
{ text: 'Flatten PDF', link: '/tools/flatten-pdf' },
{ text: 'Remove Metadata', link: '/tools/remove-metadata' },
{ text: 'Change Permissions', link: '/tools/change-permissions' },
{ text: 'Digital Signature', link: '/tools/digital-sign-pdf' },
{ text: 'Validate Signature', link: '/tools/validate-signature-pdf' }
]
}
],
'/self-hosting/': [
{
text: 'Self-Hosting Guide',
items: [
{ text: 'Overview', link: '/self-hosting/' },
{ text: 'Docker', link: '/self-hosting/docker' },
{ text: 'Vercel', link: '/self-hosting/vercel' },
{ text: 'Netlify', link: '/self-hosting/netlify' },
{ text: 'Cloudflare Pages', link: '/self-hosting/cloudflare' },
{ text: 'AWS S3 + CloudFront', link: '/self-hosting/aws' },
{ text: 'Hostinger', link: '/self-hosting/hostinger' },
{ text: 'Nginx', link: '/self-hosting/nginx' },
{ text: 'Apache', link: '/self-hosting/apache' }
]
}
],
'/': [
{
text: 'Guide',
items: [
{ text: 'Getting Started', link: '/getting-started' },
{ text: 'Tools Reference', link: '/tools/' },
{ text: 'Self-Hosting', link: '/self-hosting/' },
{ text: 'Contributing', link: '/contributing' },
{ text: 'Commercial License', link: '/licensing' }
]
}
]
},
socialLinks: [
{ icon: 'github', link: 'https://github.com/alam00000/bentopdf' },
{ icon: 'discord', link: 'https://discord.gg/Bgq3Ay3f2w' }
],
footer: {
message: 'Dual-licensed under AGPL-3.0 and Commercial License.',
copyright: 'Copyright © 2026 BentoPDF'
},
search: {
provider: 'local'
}
}
})
-136
View File
@@ -1,136 +0,0 @@
# Contributing
Thank you for your interest in contributing to BentoPDF! This guide will help you get started.
## Getting Started
### Prerequisites
- Node.js 18+
- npm 9+
- Git
### Setup
```bash
# Fork and clone the repository
git clone https://github.com/YOUR_USERNAME/bentopdf.git
cd bentopdf
# Install dependencies
npm install
# Start development server
npm run dev
```
## Project Structure
```
bentopdf/
├── src/
│ ├── js/
│ │ ├── config/ # Tool definitions
│ │ ├── logic/ # Tool page logic
│ │ ├── utils/ # Shared utilities
│ │ └── ui.ts # UI helpers
│ ├── pages/ # Tool HTML pages
│ └── css/ # Styles
├── public/
│ ├── locales/ # Translations
│ └── pymupdf-wasm/ # WASM modules
├── docs/ # Documentation (VitePress)
└── scripts/ # Build scripts
```
## Adding a New Tool
### Step 1: Create the Logic File
Create `src/js/logic/your-tool-page.ts`:
```typescript
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { state } from '../state.js';
document.addEventListener('DOMContentLoaded', () => {
// Your tool logic here
});
```
### Step 2: Create the HTML Page
Create `src/pages/your-tool.html` following the existing tool templates.
### Step 3: Register the Tool
Add to `src/js/config/tools.ts`:
```typescript
{
href: import.meta.env.BASE_URL + 'your-tool.html',
name: 'Your Tool',
icon: 'ph-icon-name',
subtitle: 'Description of your tool.',
}
```
### Step 4: Add to Build Config
Add the page to `vite.config.ts`:
```typescript
'your-tool': resolve(__dirname, 'src/pages/your-tool.html'),
```
### Step 5: Add Translations
Add entries to all locale files in `public/locales/`:
```json
{
"yourTool": {
"name": "Your Tool",
"subtitle": "Description"
}
}
```
## Code Style
- Use TypeScript for all new code
- Follow existing patterns for consistency
- Run `npm run format` before committing
- No comments in production code (comments removed during build)
## Pull Request Process
1. Create a feature branch: `git checkout -b feature/your-feature`
2. Make your changes
3. Test thoroughly: `npm run build`
4. Submit a PR with a clear description
## Translation
See [TRANSLATION.md](https://github.com/alam00000/bentopdf/blob/main/TRANSLATION.md) for adding new languages.
## Contributor License Agreement
Before your PR can be merged, you'll need to sign our CLA:
- [Individual CLA (ICLA)](https://github.com/alam00000/bentopdf/blob/main/ICLA.md)
- [Corporate CLA (CCLA)](https://github.com/alam00000/bentopdf/blob/main/CCLA.md)
## Getting Help
- [GitHub Issues](https://github.com/alam00000/bentopdf/issues)
- [Discord Community](https://discord.gg/Bgq3Ay3f2w)
## License
BentoPDF is dual-licensed:
- **AGPL-3.0** for open-source projects where you share your full source code publicly
- **Commercial License** for proprietary/closed-source applications - [Get Lifetime License for $79](https://ko-fi.com/s/f32ca4cb75)
By contributing, you agree that your contributions will be licensed under these terms.
-80
View File
@@ -1,80 +0,0 @@
# Getting Started
Welcome to BentoPDF! This guide will help you get up and running quickly.
## What is BentoPDF?
BentoPDF is a free, open-source, privacy-first PDF toolkit that runs **entirely in your browser**. Your files never leave your device—all processing happens locally using WebAssembly (WASM) technology.
## Quick Start
### Option 1: Use the Hosted Version
Visit [bentopdf.com](https://bentopdf.com) to use BentoPDF instantly—no installation required.
### Option 2: Self-Host with Docker
> [!IMPORTANT]
> Office file conversion requires `SharedArrayBuffer`, which needs both:
>
> - `Cross-Origin-Opener-Policy: same-origin`
> - `Cross-Origin-Embedder-Policy: require-corp`
> - a secure context
>
> `http://localhost` works for local testing because browsers treat loopback as trustworthy. `http://192.168.x.x` or other LAN IPs usually do not, so Word/Excel/PowerPoint conversions will require HTTPS when accessed from other devices on your network.
```bash
# Pull and run the Docker image (self-hosted build)
docker run -d -p 3000:8080 ghcr.io/alam00000/bentopdf-simple:latest
# Or use Docker Compose
curl -O https://raw.githubusercontent.com/alam00000/bentopdf/main/docker-compose.yml
docker compose up -d
```
> [!TIP]
> BentoPDF ships in two builds: the **Self-Hosted build** (`bentopdf-simple`, used above) for internal/team/organization deployments, and the **Commercial build** (`bentopdf:latest`) used by bentopdf.com itself and by commercial license holders running public-facing deployments. Both ship the same PDF tools — the Commercial build adds the bentopdf.com hero, FAQ, testimonials, and footer. If in doubt, use the Self-Hosted build.
Then open `http://localhost:3000` in your browser.
> [!NOTE]
> If you are preparing an air-gapped OCR deployment, you must host the OCR text-layer fonts internally in addition to the Tesseract worker, core runtime, and traineddata files. The full setup is documented in [Self-Hosting](/self-hosting/), including `VITE_OCR_FONT_BASE_URL` and the bundled `ocr-fonts/` directory.
### Option 3: Build from Source
```bash
# Clone the repository
git clone https://github.com/alam00000/bentopdf.git
cd bentopdf
# Install dependencies
npm install
# Start development server
npm run dev
```
## Features at a Glance
| Category | Tools |
| -------------------- | --------------------------------------------------------------- |
| **Convert to PDF** | Word, Excel, PowerPoint, Images, Markdown, EPUB, MOBI, and more |
| **Convert from PDF** | JPG, PNG, Text, Excel, SVG, and more |
| **Edit & Annotate** | Sign, Highlight, Redact, Fill Forms, Add Stamps |
| **Organize** | Merge, Split, Rotate, Delete Pages, Reorder |
| **Optimize** | Compress, Repair, Flatten, OCR |
| **Security** | Encrypt, Decrypt, Remove Restrictions |
## Browser Support
BentoPDF works best on modern browsers:
- ✅ Chrome/Edge 90+
- ✅ Firefox 90+
- ✅ Safari 15+
## Next Steps
- [Explore all tools](/tools/)
- [Self-host BentoPDF](/self-hosting/)
- [Contribute to the project](/contributing)
-42
View File
@@ -1,42 +0,0 @@
---
# https://vitepress.dev/reference/default-theme-home-page
layout: home
hero:
name: 'BentoPDF'
text: 'Free, Open-Source PDF Tools'
tagline: Process PDFs entirely in your browser. No uploads. No servers. Complete privacy.
actions:
- theme: brand
text: Get Started
link: /getting-started
- theme: alt
text: View Tools
link: /tools/
- theme: alt
text: Self-Host
link: /self-hosting/
- theme: alt
text: Commercial License
link: /licensing
features:
- icon: 🔒
title: 100% Private
details: Your files never leave your device. All processing happens locally in your browser using WebAssembly.
- icon: ⚡
title: Fast & Free
details: No sign-ups, no limits, no watermarks. Process unlimited PDFs for free, forever.
- icon: 🛠️
title: 50+ Tools
details: Convert, edit, merge, split, compress, sign, OCR, and more. Everything you need in one place.
- icon: 🌐
title: Self-Hostable
details: Deploy on your own infrastructure. Docker, Vercel, Netlify, AWS, or fully air-gapped environments with self-hosted OCR workers, language data, and text-layer fonts.
---
## Offline OCR
If you self-host BentoPDF in an air-gapped or offline environment, OCR needs more than the Tesseract worker and traineddata files. Searchable PDF output also needs the OCR text-layer fonts to be served internally.
See [Self-Hosting](/self-hosting/) for the full setup, including `VITE_OCR_FONT_BASE_URL`, the bundled `ocr-fonts/` directory, and the updated air-gap workflow.
-92
View File
@@ -1,92 +0,0 @@
# Commercial License
BentoPDF is dual-licensed under **AGPL-3.0** (for open-source projects) and a **Commercial License** (for proprietary/closed-source applications).
::: tip Full Details
For complete licensing information, delivery details, AGPL component notices, and invoicing, visit our [Licensing Page](https://bentopdf.com/licensing.html).
:::
## Get a Commercial License
**[Get Lifetime License for $79](https://ko-fi.com/s/f32ca4cb75)** — A one-time purchase that includes all future updates forever.
## When Do You Need a Commercial License?
| Use Case | License Required |
| ------------------------------------------- | ---------------------- |
| Open-source project with public source code | AGPL-3.0 (Free) |
| Internal company tool (not distributed) | AGPL-3.0 (Free) |
| Proprietary/closed-source application | **Commercial License** |
| SaaS product without source code disclosure | **Commercial License** |
| Redistributing without AGPL compliance | **Commercial License** |
## Delivery & Licensing Model
- **No license key is required or provided.** BentoPDF intentionally does not use license keys to streamline deployment and reduce operational friction for users.
- Upon purchase, users receive a ZIP archive containing the complete BentoPDF source code and binaries.
- The source code provided is identical to the code published in our public open-source repository.
**GitHub Repository:** [github.com/alam00000/bentopdf](https://github.com/alam00000/bentopdf)
## Important Notice on Third-Party Components
::: info AGPL Components — Pre-configured via CDN
BentoPDF **does not bundle** AGPL-licensed processing libraries in its source code. These components are loaded at runtime from CDN URLs that are **pre-configured by default** — all features work out of the box with zero setup.
| Component | License | Status |
| --------------- | -------- | ---------------------- |
| **PyMuPDF** | AGPL-3.0 | Pre-configured via CDN |
| **Ghostscript** | AGPL-3.0 | Pre-configured via CDN |
| **CoherentPDF** | AGPL-3.0 | Pre-configured via CDN |
WASM module URLs are configured via environment variables at build time (`.env.production`). The defaults point to jsDelivr CDN. For custom deployments (air-gapped, self-hosted), you can override via environment variables, Docker build args, or per-user via **Advanced Settings** in the UI.
See [Self-Hosting > WASM Configuration](/self-hosting/#wasm-configuration-agpl-components) for details.
This approach ensures:
- BentoPDF's core code remains under its dual-license (AGPL-3.0 / Commercial)
- WASM binaries are loaded at runtime, not included in the source
- Clear compliance boundaries for commercial users
:::
::: tip Commercial License & AGPL Features
The commercial license covers BentoPDF's own code. If you configure and use AGPL components (PyMuPDF, Ghostscript, CoherentPDF), you must still comply with their respective AGPL-3.0 license terms, which may require source code disclosure if you distribute modified versions.
:::
## Invoicing
- Ko-fi does not automatically issue invoices.
- An official invoice will be provided immediately upon request.
**Contact us:** [[email protected]](mailto:[email protected]) with your purchase details.
## What's Included
| Feature | Included |
| ----------------------------- | -------------- |
| Full source code | ✅ |
| All 50+ PDF tools | ✅ |
| Self-hosting rights | ✅ |
| Lifetime updates | ✅ |
| Remove branding (Simple Mode) | ✅ |
| Commercial support | ✅ (via email) |
| Priority feature requests | ✅ |
## FAQ
### Do I need a license key to use BentoPDF?
No. BentoPDF does not use license keys. The commercial license grants you legal rights to use the software in proprietary applications.
### Can I use BentoPDF in my SaaS product?
Yes, with a commercial license. Without it, you must comply with AGPL-3.0, which requires you to make your full source code available to users.
### What about the AGPL components?
Components like CoherentPDF are licensed under AGPL v3 and remain under that license. The commercial license covers BentoPDF's own code but does not override third-party AGPL obligations.
### How do I get an invoice?
Email [[email protected]](mailto:[email protected]) with your Ko-fi purchase confirmation, and we'll send an official invoice.
-7
View File
@@ -1,7 +0,0 @@
<svg width="166" height="166" viewBox="0 0 166 166" fill="none" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(40, 0)">
<path d="M56.7848 80.9116L0 25.6211L24.8434 1.43151C26.8037 -0.47717 29.9814 -0.47717 31.9414 1.43151L85.177 53.2662L56.7848 80.9116Z" fill="#A5B4FC"/>
<path d="M-1.30805e-05 80.7335L21.2939 101.467L42.5878 80.7335L21.2939 60L-1.30805e-05 80.7335Z" fill="#E0E7FF"/>
<path d="M56.7848 83L81.6279 107.19C83.5881 109.098 83.5881 112.192 81.6279 114.101L28.3925 165.936L0 138.29L56.7848 83Z" fill="#6366F1"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 550 B

-7
View File
@@ -1,7 +0,0 @@
<svg width="166" height="166" viewBox="0 0 166 166" fill="none" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(40, 0)">
<path d="M56.7848 80.9116L0 25.6211L24.8434 1.43151C26.8037 -0.47717 29.9814 -0.47717 31.9414 1.43151L85.177 53.2662L56.7848 80.9116Z" fill="#A5B4FC"/>
<path d="M-1.30805e-05 80.7335L21.2939 101.467L42.5878 80.7335L21.2939 60L-1.30805e-05 80.7335Z" fill="#E0E7FF"/>
<path d="M56.7848 83L81.6279 107.19C83.5881 109.098 83.5881 112.192 81.6279 114.101L28.3925 165.936L0 138.29L56.7848 83Z" fill="#6366F1"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 550 B

-275
View File
@@ -1,275 +0,0 @@
# Deploy with Apache
Host BentoPDF using Apache HTTP Server.
## Prerequisites
- Apache 2.4+
- mod_rewrite enabled
- SSL certificate (recommended)
## Step 1: Build the Project
```bash
git clone https://github.com/alam00000/bentopdf.git
cd bentopdf
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
sudo mkdir -p /var/www/bentopdf
sudo cp -r dist/* /var/www/bentopdf/
sudo chown -R www-data:www-data /var/www/bentopdf
```
## Step 3: Apache Configuration
Create `/etc/apache2/sites-available/bentopdf.conf`:
```apache
<VirtualHost *:80>
ServerName your-domain.com
Redirect permanent / https://your-domain.com/
</VirtualHost>
<VirtualHost *:443>
ServerName your-domain.com
DocumentRoot /var/www/bentopdf
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/your-domain.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/your-domain.com/privkey.pem
<Directory /var/www/bentopdf>
Options -Indexes +FollowSymLinks
AllowOverride All
Require all granted
</Directory>
# WASM MIME type
AddType application/wasm .wasm
# Prevent double-compression of pre-compressed files
SetEnvIfNoCase Request_URI "\.gz$" no-gzip
# Compression
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/plain text/css application/javascript application/json application/wasm
</IfModule>
# Cache headers
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType application/javascript "access plus 1 year"
ExpiresByType text/css "access plus 1 year"
ExpiresByType application/wasm "access plus 1 year"
ExpiresByType image/png "access plus 1 year"
ExpiresByType image/svg+xml "access plus 1 year"
</IfModule>
# 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
<FilesMatch "soffice\.wasm\.gz$">
ForceType application/wasm
Header set Content-Encoding "gzip"
</FilesMatch>
<FilesMatch "soffice\.data\.gz$">
ForceType application/octet-stream
Header set Content-Encoding "gzip"
</FilesMatch>
</VirtualHost>
```
## Step 4: .htaccess for Routing
Create `/var/www/bentopdf/.htaccess`:
```apache
RewriteEngine On
RewriteBase /
# Existing files/dirs - serve directly
RewriteCond %{REQUEST_FILENAME} -f [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^ - [L]
# ============================================
# LANGUAGE ROUTES
# ============================================
# Supported languages: de, es, zh, zh-TW, vi, it, id, tr, fr, pt
# English has no prefix - served from root
# English prefix redirects to root
RewriteRule ^en/?$ / [R=301,L]
RewriteRule ^en/(.+)$ /$1 [R=301,L]
# Language prefix root (e.g., /de/ -> /de/index.html)
RewriteCond %{DOCUMENT_ROOT}/$1/index.html -f
RewriteRule ^(de|es|zh|zh-TW|vi|it|id|tr|fr|pt)/?$ /$1/index.html [L]
# Language prefix with path (e.g., /de/merge-pdf -> /de/merge-pdf.html)
RewriteCond %{DOCUMENT_ROOT}/$1/$2.html -f
RewriteRule ^(de|es|zh|zh-TW|vi|it|id|tr|fr|pt)/([^/]+)/?$ /$1/$2.html [L]
# ============================================
# ADD .HTML EXTENSION (ROOT LEVEL)
# ============================================
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME}.html -f
RewriteRule ^([^/]+)$ $1.html [L]
ErrorDocument 404 /404.html
```
## Step 5: Enable Required Modules
```bash
sudo a2enmod rewrite
sudo a2enmod ssl
sudo a2enmod headers
sudo a2enmod expires
sudo a2enmod deflate
```
## Step 6: Enable the Site
```bash
sudo a2ensite bentopdf.conf
sudo apache2ctl configtest
sudo systemctl reload apache2
```
## Subdirectory Deployment
To host at `/pdf/`:
1. Build with base URL:
```bash
BASE_URL=/pdf/ npm run build
```
2. Update `.htaccess`:
```apache
RewriteEngine On
RewriteBase /pdf/
# Existing files/dirs - serve directly
RewriteCond %{REQUEST_FILENAME} -f [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^ - [L]
# Language routes
RewriteRule ^en/?$ /pdf/ [R=301,L]
RewriteRule ^en/(.+)$ /pdf/$1 [R=301,L]
RewriteCond %{DOCUMENT_ROOT}/pdf/$1/index.html -f
RewriteRule ^(de|es|zh|zh-TW|vi|it|id|tr|fr|pt)/?$ /pdf/$1/index.html [L]
RewriteCond %{DOCUMENT_ROOT}/pdf/$1/$2.html -f
RewriteRule ^(de|es|zh|zh-TW|vi|it|id|tr|fr|pt)/([^/]+)/?$ /pdf/$1/$2.html [L]
# Root level .html extension
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME}.html -f
RewriteRule ^([^/]+)$ $1.html [L]
ErrorDocument 404 /pdf/404.html
```
## Troubleshooting
### WASM 404 Errors
Ensure MIME type is configured:
```apache
AddType application/wasm .wasm
```
### Sign PDF or Form Filler Shows a Blank Viewer (`.mjs` MIME error)
If the browser console shows:
```
Failed to load module script: The server responded with a non-JavaScript
MIME type of "application/octet-stream". Strict MIME type checking is
enforced for module scripts per HTML spec.
```
…and the failing request ends in `.mjs`, your Apache config is missing the `.mjs → application/javascript` mapping. The bundled PDF viewer ships ES module files with a `.mjs` extension, and Apache's stock `mime.types` doesn't include them by default.
Add to your VirtualHost or `.htaccess`:
```apache
AddType application/javascript .mjs
```
Then reload Apache:
```bash
sudo apache2ctl configtest && sudo systemctl reload apache2
```
To confirm: in browser DevTools → Network tab, find the failing `.mjs` request and check the `Content-Type` response header. It should now be `application/javascript`, not `application/octet-stream`.
### Rewrite Not Working
Check that mod_rewrite is enabled:
```bash
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"
```
It also needs a secure context. `http://localhost` works for local testing, but `http://192.168.x.x` or other LAN IPs usually require HTTPS. If the headers are present but `window.crossOriginIsolated` is still `false`, check whether the page is being opened over plain HTTP on a non-loopback origin.
The pre-compressed `.wasm.gz` and `.data.gz` files also need correct `Content-Encoding`:
```apache
<FilesMatch "soffice\.wasm\.gz$">
ForceType application/wasm
Header set Content-Encoding "gzip"
</FilesMatch>
<FilesMatch "soffice\.data\.gz$">
ForceType application/octet-stream
Header set Content-Encoding "gzip"
</FilesMatch>
```
Ensure `mod_headers` is enabled: `sudo a2enmod headers`
### Permission Denied
```bash
sudo chown -R www-data:www-data /var/www/bentopdf
sudo chmod -R 755 /var/www/bentopdf
```
-200
View File
@@ -1,200 +0,0 @@
# Deploy to AWS S3 + CloudFront
Host BentoPDF on AWS for maximum control and scalability.
## Architecture
```
User → CloudFront (CDN) → S3 (Static Files)
```
## Step 1: Create S3 Bucket
```bash
# Create bucket
aws s3 mb s3://your-bentopdf-bucket --region us-east-1
# Enable static website hosting
aws s3 website s3://your-bentopdf-bucket \
--index-document index.html \
--error-document index.html
```
## Step 2: Build and Upload
```bash
# 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
aws s3 sync dist/ s3://your-bentopdf-bucket \
--delete \
--cache-control "max-age=31536000"
# Set correct MIME types for WASM
aws s3 cp s3://your-bentopdf-bucket/ s3://your-bentopdf-bucket/ \
--recursive \
--exclude "*" \
--include "*.wasm" \
--content-type "application/wasm" \
--metadata-directive REPLACE
# Set correct MIME type for ES module files (.mjs)
# S3 defaults .mjs to application/octet-stream, which browsers refuse to
# execute as ES modules. The bundled PDF viewer (Sign PDF, Form Filler)
# ships ES modules with .mjs extensions and will fail without this step.
aws s3 cp s3://your-bentopdf-bucket/ s3://your-bentopdf-bucket/ \
--recursive \
--exclude "*" \
--include "*.mjs" \
--content-type "application/javascript" \
--metadata-directive REPLACE
```
## Step 3: Create CloudFront Distribution
```bash
aws cloudfront create-distribution \
--origin-domain-name your-bentopdf-bucket.s3.amazonaws.com \
--default-root-object index.html
```
Or use the AWS Console:
1. Go to CloudFront → Create distribution
2. Origin domain: Select your S3 bucket
3. Enable "Origin Access Control"
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:
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowCloudFrontAccess",
"Effect": "Allow",
"Principal": {
"Service": "cloudfront.amazonaws.com"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::your-bentopdf-bucket/*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::ACCOUNT_ID:distribution/DISTRIBUTION_ID"
}
}
}
]
}
```
## Step 5: Custom Error Pages
Configure 404 to return `index.html` for SPA routing:
1. CloudFront → Error pages
2. Create custom error response:
- HTTP error code: 404
- Response page path: `/index.html`
- HTTP response code: 200
## Cost Estimation
| 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.
:::
## Automation with Terraform
```hcl
# main.tf
resource "aws_s3_bucket" "bentopdf" {
bucket = "your-bentopdf-bucket"
}
resource "aws_cloudfront_distribution" "bentopdf" {
origin {
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"
}
}
```

Some files were not shown because too many files have changed in this diff Show More