Lightweight, 100% local PDF editor with a web UI to open, reorder, combine, split, and stamp documents — no external services required.
PDF_JM Web is a local PDF editor with a browser-based interface. It runs a
FastAPI backend on 127.0.0.1 and serves a vanilla HTML/CSS/JS frontend, so
your documents never leave your machine. Open, order, combine, split, and save
PDFs without depending on external services.
- Open PDFs from the native system picker or from the browser
- Thumbnail view of all pages
- Select individual pages, multiple pages, or ranges with
Shift + click - Reorder pages with buttons or drag & drop
- Rotate, duplicate, and delete pages
- Insert pages from another PDF or from images
- Extract selected pages into a new PDF
- Split the document into several parts (save individually or as a ZIP)
- Merge several PDFs into one
- Stamp a 10-digit number onto selected pages
- Detect and remove likely-blank pages (analysis by text, marks, and visual sampling)
- Attach, extract, and delete embedded files in the PDF
- Save with export presets (profile, compression, and linearization)
- Overwrite the original file when saving, or create an edited copy (your choice)
- Save the document to a local folder
- Inspect a page in high resolution with an on-demand zoomed preview
- Light / dark theme and recent files
- Home: open a PDF, see recent files, and enter the main flow
- Editor:
- top bar with the main actions
- selection strip with quick status
- compact summary with
Pages,Selection,Changes, andHistory - main page grid with 4 columns on desktop and responsive fallbacks
- per-page magnifier button opening a centered HD preview
- Modals: save, insert, extract, split, merge, attachments, blank-page cleanup, metadata, bookmarks, shortcuts, and zoomed view
↑ / ↓— move the current pageSpace— select the current pageShift + click— select a rangeCtrl + A— select or clear everythingCtrl + E— open extractionCtrl + S— open saveCtrl + D— toggle light / dark themeR— rotate the current pageDelete / Backspace— delete the selectionEsc— close a modal or clear the selection
- FastAPI + Uvicorn
- PyMuPDF (
fitz) tkinterfor optional native pickers (run in a subprocess)- Vanilla HTML + CSS + JavaScript (no frameworks, no build step)
- Python 3.10 or higher
- Windows 10/11 (tested)
- Linux/macOS should work with minor adjustments in a local environment
-
Clone or download the repository
-
Create a virtual environment:
python -m venv venv
-
Activate the virtual environment:
- Windows:
./venv/Scripts/activate - Linux/macOS:
source venv/bin/activate
- Windows:
-
Install dependencies:
pip install -r requirements.txt
python main.pyThe application starts at:
http://127.0.0.1:8080- API docs:
http://127.0.0.1:8080/docs
main.py also tries to open the default browser automatically.
A config.json is generated automatically next to the code (or next to the
.exe in packaged builds) with these values:
| Key | Default | Description |
|---|---|---|
theme |
dark |
Initial theme (dark/light) |
default_output_dir |
~/Documents |
Suggested folder when saving |
thumbnail_dpi / thumbnail_width |
100 / 200 |
Thumbnail rendering |
max_file_size_mb |
100 |
File upload limit |
history_limit |
20 |
Maximum undo/redo snapshots |
session_ttl_minutes |
120 |
Minutes of inactivity before a session expires |
history_max_bytes |
536870912 |
Undo/redo memory cap per document |
recent_files |
[] |
Recent files (maximum 10) |
export_presets |
[] |
Saved export presets |
PDF_JM/
├── main.py # Entry point: starts the app at http://127.0.0.1:8080
├── backend/
│ ├── app.py # HTTP app, local-protection middleware and asset serving
│ ├── schemas.py # DTOs / API contracts
│ ├── routers/
│ │ ├── documents.py # Editing and export endpoints
│ │ └── system.py # Preferences, recents and native pickers
│ └── services/
│ └── document_service.py # In-memory state and document orchestration
├── core/
│ ├── pdf_manager.py # Base PDF manipulation with PyMuPDF
│ ├── page_operations.py # High-level page operations
│ └── utils.py # Validations, formats and native pickers
├── web/
│ ├── index.html # HTML shell
│ ├── main.js # Web UI and API interaction
│ ├── api.js # Frontend HTTP client
│ ├── modal_feedback.mjs # Busy/feedback state for modals
│ └── styles.css # Visual system and layout
├── tests/
│ ├── test_web_api.py # API and state regressions
│ ├── test_pdf_manager.py # Preview rendering regressions
│ └── test_web_ui_state.py # UI states via Node runners (.mjs)
├── config.py # Persisted preferences
├── requirements.txt # Python dependencies
└── scripts/
└── build_exe.py # Windows packaging with PyInstaller
The app listens only on 127.0.0.1, and the API is hardened with a middleware that:
- Rejects requests whose
Hostheader is notlocalhost/127.0.0.1(blocks DNS rebinding). - Rejects mutating requests (POST/PUT/PATCH/DELETE) with a non-local
Originheader (blocks CSRF from external web pages). - Adds security headers: CSP,
X-Content-Type-Options: nosniff,X-Frame-Options: DENY,Referrer-Policy.
Local save filenames are sanitized (no .. or path separators), and all
uploads respect the configured limit (max_file_size_mb).
Python syntax:
python -m py_compile main.py backend/app.py backend/schemas.py backend/routers/system.py backend/routers/documents.py backend/services/document_service.py core/pdf_manager.py core/page_operations.py core/utils.py scripts/build_exe.pyFrontend syntax:
node --check web/main.js
node --check web/api.jsTests:
pytest tests/test_pdf_manager.py tests/test_web_api.py tests/test_web_ui_state.pytest_web_ui_state.py requires node on the PATH (it runs the tests/*.mjs runners).
python scripts/build_exe.pyThe .exe keeps its config.json in the same folder as the executable.
- It does not edit text already present in the PDF
- Password-protected PDFs are not supported
- PDF forms may not keep all of their interactive behavior
- Large PDFs may take longer to generate the initial thumbnails
- Open documents are kept in memory while the session is active; a session
idle for more than
session_ttl_minutes(120 by default) is discarded, and the undo history is trimmed by count (history_limit) and by accumulated bytes (history_max_bytes, 512 MB per document)
Distributed under the GNU Affero General Public License v3.0 (AGPL-3.0), as required by its use of the AGPL-licensed PyMuPDF library. See LICENSE for the full license text.
Copyright (c) 2026 Jose Miguel Maldonado Garcia
Jose Miguel Maldonado Garcia — @JoanMike

