mirror of
https://github.com/alam00000/bentopdf.git
synced 2026-11-05 11:52:48 +01:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a729c3681 |
@@ -1,10 +0,0 @@
|
||||
node_modules
|
||||
npm-debug.log
|
||||
dist
|
||||
coverage
|
||||
.git
|
||||
.gitignore
|
||||
.env
|
||||
*.md
|
||||
.vscode
|
||||
.idea
|
||||
@@ -1,2 +0,0 @@
|
||||
# Dev-only: route TSA/certificate requests through the built-in Vite CORS proxy
|
||||
VITE_CORS_PROXY_URL=/cors-proxy
|
||||
@@ -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=
|
||||
@@ -1,2 +0,0 @@
|
||||
github: alam00000
|
||||
ko_fi: alio01
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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]"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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'
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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'
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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/
|
||||
@@ -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 +0,0 @@
|
||||
npx lint-staged
|
||||
@@ -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/
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"semi": true,
|
||||
"trailingComma": "es5",
|
||||
"singleQuote": true,
|
||||
"printWidth": 80,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
DS-0002
|
||||
|
||||
KSV-0125
|
||||
|
||||
DS-0013
|
||||
@@ -1 +0,0 @@
|
||||
https://www.bentopdf.com/funding.json
|
||||
@@ -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>
|
||||
@@ -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.*
|
||||
@@ -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
@@ -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
@@ -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;"]
|
||||
@@ -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;"]
|
||||
@@ -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.*
|
||||
@@ -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/>.
|
||||
-317
@@ -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
@@ -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
@@ -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
|
||||
@@ -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
@@ -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
@@ -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.
Binary file not shown.
@@ -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>
|
||||
@@ -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's PDF editor</a
|
||||
>, pick your file, make your changes, and download the result. The PDF
|
||||
stays on your device. There'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'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'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're working on it.
|
||||
</p>
|
||||
<p class="text-gray-300 leading-relaxed mb-5">
|
||||
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.
|
||||
</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'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'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'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's form filler render them properly.
|
||||
</p>
|
||||
<p class="text-gray-300 leading-relaxed mb-5">
|
||||
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
|
||||
<a class="text-indigo-300 hover:text-indigo-400" href="/edit-pdf"
|
||||
>PDF editor</a
|
||||
>
|
||||
and type into them. If you'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'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'll need OCR first.
|
||||
<a class="text-indigo-300 hover:text-indigo-400" href="/ocr-pdf"
|
||||
>BentoPDF'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'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'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.
|
||||
</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'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'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'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's
|
||||
developer tools. The website is how you get the software; it doesn'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't simply edit them because there isn't any text there yet.
|
||||
</p>
|
||||
<p class="text-gray-300 leading-relaxed mb-5">
|
||||
You'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'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't an imposed file-size limit.
|
||||
</p>
|
||||
<p class="text-gray-300 leading-relaxed mb-5">
|
||||
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.
|
||||
</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't specific to BentoPDF; it'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>
|
||||
@@ -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>
|
||||
@@ -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.
|
||||
@@ -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/
|
||||
@@ -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
@@ -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
|
||||
```
|
||||
@@ -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 }}
|
||||
@@ -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 }}
|
||||
@@ -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: {}
|
||||
@@ -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 }}
|
||||
@@ -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 }}
|
||||
@@ -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 }}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 |
|
||||
@@ -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',
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -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',
|
||||
},
|
||||
}
|
||||
);
|
||||
},
|
||||
};
|
||||
@@ -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" }
|
||||
# ]
|
||||
@@ -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
@@ -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>
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"label": "Docker Downloads",
|
||||
"message": "15.1M",
|
||||
"color": "blue",
|
||||
"namedLogo": "docker"
|
||||
}
|
||||
-283
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
-10696
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
-13806
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
-3
@@ -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
@@ -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": []
|
||||
}
|
||||
-1693
File diff suppressed because it is too large
Load Diff
-7
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
-1813
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
Vendored
-347
@@ -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
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": [],
|
||||
"sourcesContent": [],
|
||||
"mappings": "",
|
||||
"names": []
|
||||
}
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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 |
@@ -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 |
@@ -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
|
||||
```
|
||||
@@ -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
Reference in New Issue
Block a user