A mobile-first web app for processing document photos and uploading them to Paperless-ngx as multi-page documents.
Take photos of documents with your phone, and Paperless Feeder will automatically detect, crop, deskew, and clean them up into scan-quality pages — then upload the assembled PDF to your Paperless-ngx instance.
| Empty state | With pages |
|---|---|
![]() |
![]() |
| Input | Output |
|---|---|
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
- Capture — Take a photo or pick an image from your phone
- Detection — DocAligner heatmap regression model locates the four document corners
- Perspective crop — Warps the detected quadrilateral into a rectangle
- Deskew — Hough-based skew correction straightens residual rotation
- Enhance — Shadow removal (illumination flattening over an ink-masked background estimate), paper white balance, white-point scaling, S-curve contrast, and mild sharpening turn the photo into a scan-quality page
- Tag — Optionally attach one or more Paperless-ngx tags from a searchable popover
- Assemble — Combine multiple pages into a single PDF
- Upload — Send the PDF to Paperless-ngx via its API
FastAPI, HTMX, Alpine.js, Tailwind CSS, Pillow (PDF assembly)
Frontend libraries are vendored under app/static/vendor/ (pinned versions,
no CDN at runtime). Tailwind is compiled with the standalone CLI
— no Node toolchain anywhere.
Requires Python 3.12+.
cp .env.example .env
# Edit .env with your Paperless-ngx URL and API tokenThe app uses a single API token for all requests. The user that owns the token must be able to view all tags you want to pick from in the label selector, otherwise the popover will only show the subset they can see.
Paperless-ngx filters /api/tags/ by per-object view permission (via
django-guardian), so in a multi-user setup it is not enough for the user to
have the documents.view_tag model permission — each tag object must also
grant view_tag to that user (directly or via a group).
Recommended setup:
-
Create a dedicated Paperless user (e.g.
paperless-feeder) and generate an API token for it — put the token inPAPERLESS_TOKEN. -
Put that user in a shared group (e.g.
doc-editors). -
Make sure every tag you want visible is shared with that group (
view_tag+change_tag). For existing tags, the quickest path is a PATCH per tag as an admin:curl -X PATCH -H "Authorization: Token <admin-token>" \ -H "Content-Type: application/json" \ "$PAPERLESS_URL/api/tags/<id>/" \ -d '{"set_permissions":{"view":{"users":[],"groups":[<group-id>]},"change":{"users":[],"groups":[<group-id>]}}}'
-
To make new tags auto-share with the group, set Default permissions for new objects in the web UI (Settings → Permissions) of whichever user actually creates tags. Note that this is a client-side default applied by the web UI only — tags created by other API clients (e.g. paperless-gpt) will still need permissions granted separately.
The token only needs upload-capable permissions (add_document,
view_document, view_tag); it does not need to create or change tags.
uv sync
./scripts/dev.shThis runs uvicorn with --reload plus Tailwind in watch mode. The Tailwind
standalone CLI is downloaded to .cache/ on first run (no Node required);
the stylesheet is generated at app/static/tailwind.css (gitignored — the
Docker build produces its own copy).
docker compose up -d --buildThe app is available at http://localhost:8000.
The document processing pipeline can also be used directly:
uv run docprep photo.jpg output.jpg
uv run docprep photo.jpg output.jpg --debug debug/ # save intermediate imagesapp/ # FastAPI web application
├── main.py # App factory and router wiring
├── config.py # Settings (Paperless URL/token)
├── sessions.py # In-memory session management
├── processing.py # scan_document wrapper
├── rendering.py # HTMX partial HTML rendering
├── cleanup.py # Session TTL cleanup
├── routes/ # API endpoints
│ ├── pages.py # GET / (HTML shell)
│ ├── upload.py # POST /upload
│ ├── process.py # POST /process/{page_id}
│ ├── images.py # GET /pages/{page_id}/image
│ ├── pages_mgmt.py # DELETE/PUT page management
│ ├── assemble.py # POST /assemble (PDF)
│ ├── tags.py # GET /tags (proxies Paperless tags, paginated)
│ └── submit.py # POST /submit (Paperless upload, with tags)
└── templates/
└── index.html # Single-page HTMX/Alpine UI
docprep/ # Document processing library
├── cli.py # Click CLI entry point
├── scan.py # Detection, perspective crop, orchestration
├── deskew.py # Post-crop rotation correction
├── enhance.py # Shadow removal, white balance, contrast
└── debug.py # Pipeline visualization writer
tests/ # pytest test suite
uv run pytest tests/ -v








