diff --git a/bentopdf/CHANGELOG.md b/bentopdf/CHANGELOG.md new file mode 100644 index 000000000..3daec7555 --- /dev/null +++ b/bentopdf/CHANGELOG.md @@ -0,0 +1,8 @@ +# Changelog + +## 2.5.0 + +- Initial release of BentoPDF Home Assistant add-on +- Based on upstream BentoPDF v2.5.0 +- Serves 50+ client-side PDF tools via nginx on port 8080 +- Supports amd64 and aarch64 diff --git a/bentopdf/DOCS.md b/bentopdf/DOCS.md new file mode 100644 index 000000000..ab2a7a596 --- /dev/null +++ b/bentopdf/DOCS.md @@ -0,0 +1,53 @@ +# BentoPDF + +Privacy-first PDF toolkit with 50+ tools. All processing happens client-side in the browser — files never leave your device. + +## Usage + +After starting the add-on, open the web UI via the **Open Web UI** button or navigate to `https://:8443`. + +No configuration is required to get started. + +## Browser Security Warning (Expected) + +When you first open the add-on, your browser will show a security warning similar to: + +> **Warning: Potential Security Risk Ahead** — Firefox detected a potential security threat… + +or in Chrome/Edge: + +> **Your connection is not private** — NET::ERR_CERT_AUTHORITY_INVALID + +**This is expected and safe to proceed.** Here is why it happens and what to do: + +### Why does this happen? + +The add-on serves content over HTTPS using a **self-signed TLS certificate** generated locally on your Home Assistant instance. This certificate was not issued by a public Certificate Authority (CA) that browsers trust by default — it was created specifically for your installation. + +HTTPS is required because the office file conversion feature (Word, Excel, PowerPoint → PDF) uses LibreOffice compiled to WebAssembly, which requires `SharedArrayBuffer`. Browsers only allow `SharedArrayBuffer` on pages served over a secure context. Plain `http://` over a LAN IP does not qualify, but `https://` does — even with a self-signed certificate. + +### What to do + +Accept the warning once in your browser: + +- **Firefox**: Click **Advanced…** → **Accept the Risk and Continue** +- **Chrome / Edge**: Click **Advanced** → **Proceed to … (unsafe)** +- **Safari**: Click **Show Details** → **visit this website** + +You only need to do this once per browser. After accepting, the browser remembers the exception for this add-on. + +### Is it actually safe? + +Yes. The certificate secures the connection between **your browser and your own Home Assistant instance on your local network**. No data leaves your device — all PDF processing is done entirely in the browser. The warning exists only because the certificate was not signed by a global CA, not because anything malicious is happening. + +## Configuration + +| Option | Default | Description | +|--------|---------|-------------| +| `log_level` | `info` | Log verbosity: `info`, `debug`, `warn`, `error` | + +## Support + +For issues with the add-on packaging, open an issue at [github.com/ToledoEM/BentoPDF_HA_app](https://github.com/ToledoEM/BentoPDF_HA_app). + +For issues with BentoPDF itself, visit [github.com/alam00000/bentopdf](https://github.com/alam00000/bentopdf). diff --git a/bentopdf/Dockerfile b/bentopdf/Dockerfile new file mode 100644 index 000000000..7b62269e5 --- /dev/null +++ b/bentopdf/Dockerfile @@ -0,0 +1,39 @@ +# Global build args — must be declared before the first FROM to be usable in FROM instructions +ARG BUILD_FROM=ghcr.io/home-assistant/amd64-base:3.23 +ARG BUILD_VERSION=2.5.0 + +# Stage 1: Download and extract the BentoPDF dist release (includes bundled WASM) +FROM alpine:3.21 AS dist +ARG BUILD_VERSION +# hadolint ignore=DL3018 +RUN apk add --no-cache curl unzip \ + && curl -fsSL "https://github.com/alam00000/bentopdf/releases/download/v${BUILD_VERSION}/dist-${BUILD_VERSION}.zip" \ + -o /tmp/bentopdf.zip \ + && unzip /tmp/bentopdf.zip -d /tmp/dist + +# Stage 2: Final runtime image +FROM ${BUILD_FROM} + +ARG BUILD_VERSION + +# hadolint ignore=DL3018 +RUN apk add --no-cache nginx openssl bash \ + && sed -i 's/nginx:x:100:101/nginx:x:0:0/' /etc/passwd \ + && sed -i 's/^nginx:x:101:/nginx:x:0:/' /etc/group + +COPY --from=dist /tmp/dist/ /usr/share/nginx/html/ + +COPY run.sh /run.sh +COPY rootfs / + +RUN chmod +x /run.sh \ + && chmod +x /etc/s6-overlay/s6-rc.d/bentopdf/run \ + && chmod +x /etc/s6-overlay/s6-rc.d/bentopdf/finish \ + && chmod +x /etc/cont-init.d/50-ssl.sh + +LABEL \ + io.hass.version="${BUILD_VERSION}" \ + io.hass.type="addon" \ + io.hass.arch="aarch64|amd64" + +ENTRYPOINT ["/init"] diff --git a/bentopdf/README.md b/bentopdf/README.md new file mode 100644 index 000000000..9ab62dc29 --- /dev/null +++ b/bentopdf/README.md @@ -0,0 +1,111 @@ +# BentoPDF for Home Assistant + +
+ BentoPDF +
+ + + +A privacy-first PDF toolkit running entirely in your browser — no uploads, no cloud, no tracking. All processing happens locally via WebAssembly. This add-on serves the BentoPDF web app from your Home Assistant instance so you can access it from anywhere on your network. + +--- + +## Features + +### Organize & Edit +| Tool | Tool | Tool | +|------|------|------| +| Merge PDF | Split PDF | Organize PDF | +| Delete Pages | Extract Pages | Reverse Pages | +| Rotate PDF | Rotate Custom | Crop PDF | +| Add Blank Page | Divide Pages | N-Up PDF | +| Alternate Merge | Combine Single Page | PDF Booklet | +| PDF Merge & Split | Fix Page Size | | + +### Convert TO PDF +| Tool | Tool | Tool | +|------|------|------| +| Word to PDF | Excel to PDF | PowerPoint to PDF | +| Image to PDF | JPG to PDF | PNG to PDF | +| BMP to PDF | TIFF to PDF | WEBP to PDF | +| HEIC to PDF | SVG to PDF | PSD to PDF | +| Markdown to PDF | HTML / Email to PDF | RTF to PDF | +| TXT to PDF | CSV to PDF | JSON to PDF | +| XML to PDF | ODT to PDF | ODS to PDF | +| ODP to PDF | ODG to PDF | EPUB to PDF | +| MOBI to PDF | FB2 to PDF | CBZ to PDF | +| XPS to PDF | VSD to PDF | PUB to PDF | +| WPS to PDF | WPD to PDF | Pages to PDF | + +### Convert FROM PDF +| Tool | Tool | Tool | +|------|------|------| +| PDF to DOCX | PDF to Excel | PDF to JPG | +| PDF to PNG | PDF to BMP | PDF to TIFF | +| PDF to WEBP | PDF to SVG | PDF to Text | +| PDF to Markdown | PDF to JSON | PDF to CSV | +| PDF to PDF/A | PDF to ZIP | PDF to Greyscale | + +### Security & Metadata +| Tool | Tool | Tool | +|------|------|------| +| Encrypt PDF | Decrypt PDF | Change Permissions | +| Remove Restrictions | Sign PDF | Digital Sign PDF | +| Validate Signature | Edit Metadata | View Metadata | +| Remove Metadata | Sanitize PDF | Flatten PDF | +| Remove Annotations | Repair PDF | | + +### Enhance & Process +| Tool | Tool | Tool | +|------|------|------| +| Compress PDF | OCR PDF | Deskew PDF | +| Rasterize PDF | Linearize PDF | PDF to PDF/A | +| Adjust Colors | Invert Colors | Text Color | +| Background Color | Bates Numbering | Page Numbers | +| Header & Footer | Add Watermark | Add Stamps | +| Scanner Effect | Posterize PDF | Font to Outline | +| PDF Layers | Compare PDFs | Prepare for AI | + +### Forms & More +| Tool | Tool | Tool | +|------|------|------| +| Form Creator | Form Filler | Table of Contents | +| Bookmark | PDF Editor | Extract Images | +| Extract Tables | Extract Attachments | Edit Attachments | +| Add Attachments | Page Dimensions | PDF Workflow | + +--- + +## Installation + +1. Go to **Settings → Add-ons → Add-on Store** in Home Assistant. +2. Open the menu (`···`) → **Repositories**. +3. Add: `https://github.com/ToledoEM/BentoPDF_HA_app` +4. Find **BentoPDF** in the store and install it. +5. Start the add-on — the web UI is available at `http://:8080`. + +--- + +## Configuration + +| Option | Default | Description | +|--------|---------|-------------| +| `log_level` | `info` | Log verbosity: `info`, `debug`, `warn`, `error` | + +No other configuration is needed. Drop your files in and go. + +--- + +## Privacy + +- All PDF processing runs **in-browser via WebAssembly** (PyMuPDF, Ghostscript, Tesseract, LibreOffice, CPDF) +- Files are **never uploaded** to any server — not even the one running this add-on +- No telemetry, no analytics, no external requests +- Works fully **offline** once loaded + +--- + +## Support + +- Add-on issues → [github.com/ToledoEM/BentoPDF_HA_app](https://github.com/ToledoEM/BentoPDF_HA_app/issues) +- BentoPDF upstream → [github.com/alam00000/bentopdf](https://github.com/alam00000/bentopdf) diff --git a/bentopdf/apparmor.txt b/bentopdf/apparmor.txt new file mode 100644 index 000000000..9d0b322f6 --- /dev/null +++ b/bentopdf/apparmor.txt @@ -0,0 +1,42 @@ +#include + +profile bentopdf flags=(attach_disconnected,mediate_deleted) { + #include + + # Capabilities + file, + signal (send) set=(kill,term,int,hup,cont), + + # S6-Overlay + /init ix, + /bin/** ix, + /usr/bin/** ix, + /run/{s6,s6-rc*,service}/** ix, + /package/** ix, + /command/** ix, + /etc/services.d/** rwix, + /etc/cont-init.d/** rwix, + /etc/cont-finish.d/** rwix, + /run/{,**} rwk, + /dev/tty rw, + + # Bashio + /usr/lib/bashio/** ix, + /tmp/** rwk, + + # App data + /data/** rw, + + # nginx + /usr/sbin/nginx ix, + /etc/nginx/** r, + /usr/share/nginx/** r, + /var/lib/nginx/** rw, + /var/log/nginx/** rw, + /run/nginx/** rw, + + # Deny dangerous kernel interfaces + deny /proc/kcore rwklx, + deny /proc/sysrq-trigger rwklx, + deny /sys/firmware/** rwklx, +} diff --git a/bentopdf/build.yaml b/bentopdf/build.yaml new file mode 100644 index 000000000..3f1782577 --- /dev/null +++ b/bentopdf/build.yaml @@ -0,0 +1,4 @@ +--- +build_from: + aarch64: ghcr.io/home-assistant/aarch64-base:3.23 + amd64: ghcr.io/home-assistant/amd64-base:3.23 diff --git a/bentopdf/config.yaml b/bentopdf/config.yaml new file mode 100644 index 000000000..346e10942 --- /dev/null +++ b/bentopdf/config.yaml @@ -0,0 +1,31 @@ +name: "BentoPDF" +slug: bentopdf +image: ghcr.io/alexbelgium/bentopdf-{arch} +description: "Privacy-first PDF toolkit. 50+ tools, all processing client-side in the browser. Files never leave your device." +version: "2.5.0" +url: "https://github.com/alexbelgium/hassio-addons/tree/master/bentopdf" +arch: + - amd64 + - aarch64 +startup: application +init: false +stage: stable + +ports: + 8080/tcp: 8080 + 8443/tcp: 8443 +ports_description: + 8080/tcp: "BentoPDF HTTP (watchdog / redirect to HTTPS)" + 8443/tcp: "BentoPDF HTTPS Web UI" + +webui: "https://[HOST]:[PORT:8443]" +watchdog: "http://[HOST]:[PORT:8080]/" + +panel_icon: mdi:file-pdf-box +panel_title: BentoPDF + +options: + log_level: "info" + +schema: + log_level: list(info|debug|warn|error) diff --git a/bentopdf/icon.png b/bentopdf/icon.png new file mode 100644 index 000000000..5f5c3fb8f Binary files /dev/null and b/bentopdf/icon.png differ diff --git a/bentopdf/logo.png b/bentopdf/logo.png new file mode 100644 index 000000000..042c40575 Binary files /dev/null and b/bentopdf/logo.png differ diff --git a/bentopdf/rootfs/etc/cont-init.d/50-ssl.sh b/bentopdf/rootfs/etc/cont-init.d/50-ssl.sh new file mode 100644 index 000000000..b418c6ffb --- /dev/null +++ b/bentopdf/rootfs/etc/cont-init.d/50-ssl.sh @@ -0,0 +1,22 @@ +#!/usr/bin/with-contenv bash +# shellcheck shell=bash +# Generate a self-signed TLS certificate for HTTPS access on LAN. +# The cert is stored under /config/bentopdf/ssl/ so it persists across restarts. + +SSL_DIR=/config/bentopdf/ssl +CERT="${SSL_DIR}/cert.pem" +KEY="${SSL_DIR}/key.pem" + +if [ ! -f "${CERT}" ] || [ ! -f "${KEY}" ]; then + echo "[50-ssl] Generating self-signed TLS certificate..." + mkdir -p "${SSL_DIR}" + openssl req -x509 -nodes -newkey rsa:2048 \ + -keyout "${KEY}" \ + -out "${CERT}" \ + -days 3650 \ + -subj "/CN=homeassistant.local" \ + -addext "subjectAltName=DNS:homeassistant.local,DNS:localhost,IP:127.0.0.1" + echo "[50-ssl] Certificate generated at ${CERT}" +else + echo "[50-ssl] TLS certificate already exists, skipping generation." +fi diff --git a/bentopdf/rootfs/etc/nginx/nginx.conf b/bentopdf/rootfs/etc/nginx/nginx.conf new file mode 100644 index 000000000..804a9eeba --- /dev/null +++ b/bentopdf/rootfs/etc/nginx/nginx.conf @@ -0,0 +1,63 @@ +daemon off; +pid /tmp/nginx.pid; +error_log /proc/1/fd/1 warn; + +events { + worker_connections 1024; +} + +http { + client_body_temp_path /tmp/client_body; + proxy_temp_path /tmp/proxy; + fastcgi_temp_path /tmp/fastcgi; + uwsgi_temp_path /tmp/uwsgi; + scgi_temp_path /tmp/scgi; + + access_log /proc/1/fd/1; + + include /etc/nginx/mime.types; + types { + application/javascript mjs; + } + default_type application/octet-stream; + + gzip_static on; + + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_comp_level 9; + gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json application/wasm application/x-javascript text/x-component; + + # HTTP on 8080 — redirect to HTTPS; root path returns 200 for HA watchdog + server { + listen 8080; + listen [::]:8080; + server_name localhost; + + location = / { + return 200 "ok"; + } + + location / { + return 301 https://$host:8443$request_uri; + } + } + + # HTTPS on 8443 — main browser access with SharedArrayBuffer support + server { + listen 8443 ssl; + listen [::]:8443 ssl; + server_name localhost; + root /usr/share/nginx/html; + index index.html; + absolute_redirect off; + + ssl_certificate /config/bentopdf/ssl/cert.pem; + ssl_certificate_key /config/bentopdf/ssl/key.pem; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; + + include /etc/nginx/snippets/bentopdf-locations.conf; + } +} diff --git a/bentopdf/rootfs/etc/nginx/snippets/bentopdf-locations.conf b/bentopdf/rootfs/etc/nginx/snippets/bentopdf-locations.conf new file mode 100644 index 000000000..e87ac145e --- /dev/null +++ b/bentopdf/rootfs/etc/nginx/snippets/bentopdf-locations.conf @@ -0,0 +1,86 @@ +location ~ ^/(en|ar|be|da|de|es|fr|id|it|nl|pt|tr|vi|zh|zh-TW)(/.*)?$ { + try_files $uri $uri/ $uri.html /$1/index.html /index.html; + expires 5m; + add_header Cache-Control "public, must-revalidate"; + add_header Cross-Origin-Embedder-Policy "require-corp" always; + add_header Cross-Origin-Opener-Policy "same-origin" always; +} + +location ~ ^/(.+?)/(en|ar|be|da|de|es|fr|id|it|nl|pt|tr|vi|zh|zh-TW)(/.*)?$ { + try_files $uri $uri/ $uri.html /$1/$2/index.html /$1/index.html /index.html; + expires 5m; + add_header Cache-Control "public, must-revalidate"; + add_header Cross-Origin-Embedder-Policy "require-corp" always; + add_header Cross-Origin-Opener-Policy "same-origin" always; +} + +location ~* \.html$ { + expires 1h; + add_header Cache-Control "public, must-revalidate"; + add_header Cross-Origin-Embedder-Policy "require-corp" always; + add_header Cross-Origin-Opener-Policy "same-origin" always; +} + +location ~* /libreoffice-wasm/soffice\.wasm\.gz$ { + gzip off; + types {} default_type application/wasm; + add_header Content-Encoding gzip; + add_header Vary "Accept-Encoding"; + add_header Cache-Control "public, immutable"; + add_header Cross-Origin-Embedder-Policy "require-corp" always; + add_header Cross-Origin-Opener-Policy "same-origin" always; +} + +location ~* /libreoffice-wasm/soffice\.data\.gz$ { + gzip off; + types {} default_type application/octet-stream; + add_header Content-Encoding gzip; + add_header Vary "Accept-Encoding"; + add_header Cache-Control "public, immutable"; + add_header Cross-Origin-Embedder-Policy "require-corp" always; + add_header Cross-Origin-Opener-Policy "same-origin" always; +} + +location ~* \.(wasm|wasm\.gz|data|data\.gz)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + add_header Cross-Origin-Embedder-Policy "require-corp" always; + add_header Cross-Origin-Opener-Policy "same-origin" always; +} + +location ~* \.(js|mjs|css|woff|woff2|ttf|eot|otf)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + add_header Cross-Origin-Embedder-Policy "require-corp" always; + add_header Cross-Origin-Opener-Policy "same-origin" always; +} + +location ~* \.(png|jpg|jpeg|gif|ico|svg|webp|avif|mp4|webm)$ { + expires 1y; + add_header Cache-Control "public, immutable"; +} + +location ~* \.json$ { + expires 1w; + add_header Cache-Control "public, must-revalidate"; +} + +location ~* \.pdf$ { + expires 1y; + add_header Cache-Control "public, immutable"; +} + +location / { + try_files $uri $uri/ $uri.html /index.html; + expires 5m; + add_header Cache-Control "public, must-revalidate"; + add_header Cross-Origin-Embedder-Policy "require-corp" always; + add_header Cross-Origin-Opener-Policy "same-origin" always; +} + +add_header X-Frame-Options "SAMEORIGIN" always; +add_header X-Content-Type-Options "nosniff" always; +add_header X-XSS-Protection "1; mode=block" always; +add_header Cross-Origin-Opener-Policy "same-origin" always; +add_header Cross-Origin-Embedder-Policy "require-corp" always; +add_header Cross-Origin-Resource-Policy "cross-origin" always; diff --git a/bentopdf/rootfs/etc/s6-overlay/s6-rc.d/bentopdf/dependencies.d/base b/bentopdf/rootfs/etc/s6-overlay/s6-rc.d/bentopdf/dependencies.d/base new file mode 100644 index 000000000..e69de29bb diff --git a/bentopdf/rootfs/etc/s6-overlay/s6-rc.d/bentopdf/finish b/bentopdf/rootfs/etc/s6-overlay/s6-rc.d/bentopdf/finish new file mode 100644 index 000000000..2c31af381 --- /dev/null +++ b/bentopdf/rootfs/etc/s6-overlay/s6-rc.d/bentopdf/finish @@ -0,0 +1,3 @@ +#!/usr/bin/with-contenv bash +# shellcheck shell=bash +exit 0 diff --git a/bentopdf/rootfs/etc/s6-overlay/s6-rc.d/bentopdf/run b/bentopdf/rootfs/etc/s6-overlay/s6-rc.d/bentopdf/run new file mode 100644 index 000000000..781859916 --- /dev/null +++ b/bentopdf/rootfs/etc/s6-overlay/s6-rc.d/bentopdf/run @@ -0,0 +1,3 @@ +#!/usr/bin/with-contenv bash +# shellcheck shell=bash +exec /run.sh diff --git a/bentopdf/rootfs/etc/s6-overlay/s6-rc.d/bentopdf/type b/bentopdf/rootfs/etc/s6-overlay/s6-rc.d/bentopdf/type new file mode 100644 index 000000000..5883cff0c --- /dev/null +++ b/bentopdf/rootfs/etc/s6-overlay/s6-rc.d/bentopdf/type @@ -0,0 +1 @@ +longrun diff --git a/bentopdf/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/bentopdf b/bentopdf/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/bentopdf new file mode 100644 index 000000000..e69de29bb diff --git a/bentopdf/run.sh b/bentopdf/run.sh new file mode 100644 index 000000000..076bda474 --- /dev/null +++ b/bentopdf/run.sh @@ -0,0 +1,9 @@ +#!/usr/bin/with-contenv bashio +# shellcheck shell=bash +set -Eeuo pipefail + +LOG_LEVEL=$(bashio::config 'log_level' 'info') + +bashio::log.info "Starting BentoPDF (log_level=${LOG_LEVEL})" + +exec nginx diff --git a/bentopdf/translations/en.yaml b/bentopdf/translations/en.yaml new file mode 100644 index 000000000..31b79a6af --- /dev/null +++ b/bentopdf/translations/en.yaml @@ -0,0 +1,7 @@ +configuration: + log_level: + name: Log level + description: Controls the verbosity of add-on logging. + +network: + 8080/tcp: Web UI port (direct access, not used for Ingress)