From 196286783a29ea206bfba76cd25ee2912d8ba4cf Mon Sep 17 00:00:00 2001 From: 0xFEEDC0DE64 Date: Sat, 10 Oct 2026 15:41:26 +0200 Subject: [PATCH] Add authenticated PDF editor for Paperless scans --- README.md | 8 +++++- ci-deployer.yaml | 6 ++--- deploy.sh | 3 ++- install.sh | 5 ++-- pdf-editor.yaml | 65 ++++++++++++++++++++++++++++++++++++++++++++++++ test.sh | 7 +++++- 6 files changed, 86 insertions(+), 8 deletions(-) create mode 100644 pdf-editor.yaml diff --git a/README.md b/README.md index 7ef0e31..12a01f3 100644 --- a/README.md +++ b/README.md @@ -24,9 +24,15 @@ If an ADF scan immediately says `Document feeder out of documents` and `0 pages To merge separate Paperless documents imported from JPG scans, enable **Try to include archive version in merge for non-PDF files** in the merge dialog. Paperless's default merge tries to open the original JPG as a PDF and skips it. Enable **Delete original documents after successful merge** if you want only the merged entry in the active document list; the source entries go to Paperless's trash. A single feeder batch sent through the scanner action already becomes one document, without a later merge. +## Rotate or crop an existing document + +Paperless's **PDF Editor** works only when the selected file version is a PDF. For a scanned JPG, download its archived PDF from the document's download menu; Paperless creates that PDF during OCR. Open to use the self-hosted Stirling PDF editor, protected by the same Authentik gate as ScanservJS. Its **Rotate** and **Crop** tools let you turn pages and visually select the area to keep. Download the edited PDF, then return to the original Paperless document and choose **Versions → Upload new version**. The document ID, title, tags, correspondent, document type, permissions, and custom fields stay with that Paperless entry; the previous file remains available in version history. Once the new PDF version has imported, Paperless's own PDF Editor can rotate or rearrange it too. + +Paperless has no native crop tool or custom button that sends a document to another editor. Cropping a PDF changes its visible page boundary without resampling the scan image. For sensitive content outside the crop, use a redaction tool rather than relying on crop alone. Stirling PDF uses the pinned upstream Ultra-Lite image and temporary container storage; no document PVC or cloud account is involved. + ## CI/CD -`./test.sh` checks syntax, the deployment manifest, and the scanner PDF handoff. `./deploy.sh` updates the existing workloads, services, ConfigMap, and ingress using `app.yaml`. The GitLab and Gitea workflows call these same scripts. Bootstrap once with `./create-ci-kubeconfig.sh`, then save its single output line as the protected CI variable / Gitea Actions secret `KUBE_CONFIG_BASE64`. Do not commit it. The deployer has named-object read and patch permissions and cannot read Secrets. Run `./install.sh` manually for first installation or changes to `storage.yaml`, `postgresql-values.yaml`, or Authentik setup. +`./test.sh` checks syntax, the deployment manifests, and the scanner PDF handoff. `./deploy.sh` updates the existing workloads, services, ConfigMap, and ingresses using `app.yaml` and `pdf-editor.yaml`. The GitLab and Gitea workflows call these same scripts. Bootstrap once with `./create-ci-kubeconfig.sh`, then save its single output line as the protected CI variable / Gitea Actions secret `KUBE_CONFIG_BASE64`. Do not commit it. The deployer has named-object read and patch permissions and cannot read Secrets. Run `./install.sh` manually for first installation or changes to `storage.yaml`, `postgresql-values.yaml`, or Authentik setup. The Git remote is `gitea@brunner.ninja:feedc0de/paperless-ngx-deployment.git`. No credentials or generated kubeconfig are committed. To publish changes: `git add . && git commit -m 'Deploy Paperless-ngx' && git push origin main`. diff --git a/ci-deployer.yaml b/ci-deployer.yaml index 265f400..eb7d231 100644 --- a/ci-deployer.yaml +++ b/ci-deployer.yaml @@ -12,15 +12,15 @@ metadata: rules: - apiGroups: [apps] resources: [deployments] - resourceNames: [paperless, valkey, scanner] + resourceNames: [paperless, valkey, scanner, pdf-editor] verbs: [get, patch, watch] - apiGroups: [""] resources: [services] - resourceNames: [paperless, valkey, scanner] + resourceNames: [paperless, valkey, scanner, pdf-editor] verbs: [get, patch] - apiGroups: [networking.k8s.io] resources: [ingresses] - resourceNames: [paperless, scanner] + resourceNames: [paperless, scanner, pdf-editor] verbs: [get, patch] - apiGroups: [""] resources: [configmaps] diff --git a/deploy.sh b/deploy.sh index 65ad5f3..f971327 100755 --- a/deploy.sh +++ b/deploy.sh @@ -2,7 +2,7 @@ set -euo pipefail cd "$(dirname "${BASH_SOURCE[0]}")" ./test.sh -apply_output=$(kubectl apply -f app.yaml) +apply_output=$(kubectl apply -f app.yaml -f pdf-editor.yaml) printf '%s\n' "$apply_output" if [[ $apply_output == *'configmap/scanner-config configured'* ]]; then kubectl -n paperless-ngx rollout restart deployment/scanner @@ -10,3 +10,4 @@ fi kubectl -n paperless-ngx rollout status deployment/valkey --timeout=5m || { kubectl -n paperless-ngx describe deployment valkey; exit 1; } kubectl -n paperless-ngx rollout status deployment/paperless --timeout=15m || { kubectl -n paperless-ngx describe deployment paperless; exit 1; } kubectl -n paperless-ngx rollout status deployment/scanner --timeout=10m || { kubectl -n paperless-ngx describe deployment scanner; kubectl -n paperless-ngx logs deployment/scanner --tail=100; exit 1; } +kubectl -n paperless-ngx rollout status deployment/pdf-editor --timeout=10m || { kubectl -n paperless-ngx describe deployment pdf-editor; kubectl -n paperless-ngx logs deployment/pdf-editor --tail=100; exit 1; } diff --git a/install.sh b/install.sh index c70fbff..29ebcc8 100755 --- a/install.sh +++ b/install.sh @@ -21,8 +21,8 @@ fi ./setup-authentik.sh ./test.sh helm upgrade --install postgresql postgres --repo https://groundhog2k.github.io/helm-charts/ --version 1.6.7 -n paperless-ngx -f postgresql-values.yaml --wait --timeout 10m -kubectl apply --dry-run=server -f storage.yaml -f app.yaml -apply_output=$(kubectl apply -f storage.yaml -f app.yaml) +kubectl apply --dry-run=server -f storage.yaml -f app.yaml -f pdf-editor.yaml +apply_output=$(kubectl apply -f storage.yaml -f app.yaml -f pdf-editor.yaml) printf '%s\n' "$apply_output" if [[ $apply_output == *'configmap/scanner-config configured'* ]]; then kubectl -n paperless-ngx rollout restart deployment/scanner @@ -30,4 +30,5 @@ fi kubectl -n paperless-ngx rollout status deployment/valkey --timeout=5m kubectl -n paperless-ngx rollout status deployment/paperless --timeout=15m kubectl -n paperless-ngx rollout status deployment/scanner --timeout=10m +kubectl -n paperless-ngx rollout status deployment/pdf-editor --timeout=10m kubectl -n paperless-ngx exec deployment/paperless -- python manage.py shell -c 'from django.contrib.auth.models import Group; [Group.objects.get_or_create(name=name) for name in ("Paperless Users", "Paperless Admins")]' diff --git a/pdf-editor.yaml b/pdf-editor.yaml new file mode 100644 index 0000000..fa7f0e9 --- /dev/null +++ b/pdf-editor.yaml @@ -0,0 +1,65 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: pdf-editor + namespace: paperless-ngx +spec: + replicas: 1 + selector: + matchLabels: {app: pdf-editor} + template: + metadata: + labels: {app: pdf-editor} + spec: + automountServiceAccountToken: false + containers: + - name: pdf-editor + image: docker.io/stirlingtools/stirling-pdf:3.1.0-ultra-lite@sha256:b19e480530e5812f96f1cddcf467bc0e652a252de98fdd36587b33ea04e75251 + ports: [{name: http, containerPort: 8080}] + env: + - {name: SYSTEM_ROOTURIPATH, value: /pdf} + - {name: SYSTEM_ENABLEANALYTICS, value: "false"} + - {name: SECURITY_ENABLELOGIN, value: "false"} + startupProbe: + httpGet: {path: /pdf/, port: http} + periodSeconds: 5 + failureThreshold: 60 + readinessProbe: + httpGet: {path: /pdf/, port: http} + periodSeconds: 10 + livenessProbe: + httpGet: {path: /pdf/, port: http} + periodSeconds: 30 + resources: + requests: {cpu: 100m, memory: 512Mi} + limits: {memory: 2Gi} +--- +apiVersion: v1 +kind: Service +metadata: + name: pdf-editor + namespace: paperless-ngx +spec: + selector: {app: pdf-editor} + ports: [{name: http, port: 8080, targetPort: http}] +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: pdf-editor + namespace: paperless-ngx + annotations: + traefik.ingress.kubernetes.io/router.entrypoints: websecure + traefik.ingress.kubernetes.io/router.middlewares: default-authentik@kubernetescrd +spec: + ingressClassName: traefik + rules: + - host: scanner.brunner.ninja + http: + paths: + - path: /pdf + pathType: Prefix + backend: + service: + name: pdf-editor + port: {name: http} diff --git a/test.sh b/test.sh index b4140aa..351d45c 100755 --- a/test.sh +++ b/test.sh @@ -10,12 +10,17 @@ from pathlib import Path import yaml documents = [] -for path in ("storage.yaml", "app.yaml", "ci-deployer.yaml"): +for path in ("storage.yaml", "app.yaml", "pdf-editor.yaml", "ci-deployer.yaml"): with open(path, encoding="utf-8") as stream: documents.extend(doc for doc in yaml.safe_load_all(stream) if doc) images = [container["image"] for doc in documents if doc["kind"] == "Deployment" for container in doc["spec"]["template"]["spec"]["containers"]] assert images and all(":" in image and not image.endswith(":latest") for image in images) +editor_ingress = next(doc for doc in documents if doc["kind"] == "Ingress" + and doc["metadata"]["name"] == "pdf-editor") +assert editor_ingress["spec"]["rules"][0]["host"] == "scanner.brunner.ninja" +assert editor_ingress["spec"]["rules"][0]["http"]["paths"][0]["path"] == "/pdf" +assert "authentik" in editor_ingress["metadata"]["annotations"]["traefik.ingress.kubernetes.io/router.middlewares"] config = next(doc["data"]["config.local.js"] for doc in documents if doc["kind"] == "ConfigMap" and doc["metadata"]["name"] == "scanner-config") with tempfile.TemporaryDirectory() as temp: