diff --git a/.dockerignore b/.dockerignore
index 54b49fd807..50f7376686 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -5,6 +5,7 @@ frontend/dist
frontend/build
frontend/.vite
frontend/.tauri
+frontend/src-tauri/target
# Gradle build artifacts
.gradle
diff --git a/.github/workflows/PR-Demo-Comment-with-react.yml b/.github/workflows/PR-Demo-Comment-with-react.yml
index 82f1e0140e..bfde13275f 100644
--- a/.github/workflows/PR-Demo-Comment-with-react.yml
+++ b/.github/workflows/PR-Demo-Comment-with-react.yml
@@ -180,7 +180,7 @@ jobs:
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
context: .
- file: ./Dockerfile
+ file: ./docker/embedded/Dockerfile
push: true
tags: ${{ secrets.DOCKER_HUB_USERNAME }}/test:pr-${{ needs.check-comment.outputs.pr_number }}
build-args: VERSION_TAG=alpha
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 03f684c4f0..9a049bb90c 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -262,7 +262,7 @@ jobs:
strategy:
fail-fast: false
matrix:
- docker-rev: ["Dockerfile", "Dockerfile.ultra-lite", "Dockerfile.fat"]
+ docker-rev: ["docker/embedded/Dockerfile", "docker/embedded/Dockerfile.ultra-lite", "docker/embedded/Dockerfile.fat"]
steps:
- name: Harden Runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
@@ -301,7 +301,7 @@ jobs:
with:
builder: ${{ steps.buildx.outputs.name }}
context: .
- file: ./docker/backend/${{ matrix.docker-rev }}
+ file: ./${{ matrix.docker-rev }}
push: false
cache-from: type=gha
cache-to: type=gha,mode=max
diff --git a/.github/workflows/push-docker-v2.yml b/.github/workflows/push-docker-v2.yml
index 23d89ef880..5f2b70f50d 100644
--- a/.github/workflows/push-docker-v2.yml
+++ b/.github/workflows/push-docker-v2.yml
@@ -5,6 +5,7 @@ on:
push:
branches:
- V2-master
+ - alljavadocker
# cancel in-progress jobs if a new job is triggered
# This is useful to avoid running multiple builds for the same branch if a new commit is pushed
@@ -93,10 +94,10 @@ jobs:
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}
type=raw,value=latest
- - name: Generate tags for latest (V2-demo branch - test)
+ - name: Generate tags for latest (alljavadocker branch - test)
id: meta-test
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
- if: github.ref == 'refs/heads/V2-demo'
+ if: github.ref == 'refs/heads/alljavadocker'
with:
images: |
ghcr.io/stirling-tools/stirling-pdf-test
@@ -110,7 +111,7 @@ jobs:
with:
builder: ${{ steps.buildx.outputs.name }}
context: .
- file: ./docker/Dockerfile.unified
+ file: ./docker/embedded/Dockerfile
push: true
cache-from: type=gha
cache-to: type=gha,mode=max
@@ -149,10 +150,10 @@ jobs:
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}-fat
type=raw,value=latest-fat
- - name: Generate tags for latest-fat (V2-demo branch - test)
+ - name: Generate tags for latest-fat (alljavadocker branch - test)
id: meta-fat-test
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
- if: github.ref == 'refs/heads/V2-demo'
+ if: github.ref == 'refs/heads/alljavadocker'
with:
images: |
ghcr.io/stirling-tools/stirling-pdf-test
@@ -166,7 +167,7 @@ jobs:
with:
builder: ${{ steps.buildx.outputs.name }}
context: .
- file: ./docker/Dockerfile.unified
+ file: ./docker/embedded/Dockerfile.fat
push: true
cache-from: type=gha
cache-to: type=gha,mode=max
@@ -203,10 +204,10 @@ jobs:
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}-ultra-lite
type=raw,value=latest-ultra-lite
- - name: Generate tags for ultra-lite (V2-demo branch - test)
+ - name: Generate tags for ultra-lite (alljavadocker branch - test)
id: meta-lite-test
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
- if: github.ref == 'refs/heads/V2-demo'
+ if: github.ref == 'refs/heads/alljavadocker'
with:
images: |
ghcr.io/stirling-tools/stirling-pdf-test
@@ -220,7 +221,7 @@ jobs:
with:
builder: ${{ steps.buildx.outputs.name }}
context: .
- file: ./docker/Dockerfile.unified-lite
+ file: ./docker/embedded/Dockerfile.ultra-lite
push: true
cache-from: type=gha
cache-to: type=gha,mode=max
diff --git a/.github/workflows/push-docker.yml b/.github/workflows/push-docker.yml
index 48e3514333..3f2a0c8d07 100644
--- a/.github/workflows/push-docker.yml
+++ b/.github/workflows/push-docker.yml
@@ -107,7 +107,7 @@ jobs:
with:
builder: ${{ steps.buildx.outputs.name }}
context: .
- file: ./Dockerfile
+ file: ./docker/embedded/Dockerfile
push: true
cache-from: type=gha
cache-to: type=gha,mode=max
@@ -152,7 +152,7 @@ jobs:
if: github.ref != 'refs/heads/main'
with:
context: .
- file: ./Dockerfile.ultra-lite
+ file: ./docker/embedded/Dockerfile.ultra-lite
push: true
cache-from: type=gha
cache-to: type=gha,mode=max
@@ -183,7 +183,7 @@ jobs:
with:
builder: ${{ steps.buildx.outputs.name }}
context: .
- file: ./Dockerfile.fat
+ file: ./docker/embedded/Dockerfile.fat
push: true
cache-from: type=gha
cache-to: type=gha,mode=max
diff --git a/.github/workflows/testdriver.yml b/.github/workflows/testdriver.yml
index 2bd47bee34..828c84d621 100644
--- a/.github/workflows/testdriver.yml
+++ b/.github/workflows/testdriver.yml
@@ -66,7 +66,7 @@ jobs:
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
context: .
- file: ./Dockerfile
+ file: ./docker/embedded/Dockerfile
push: true
tags: ${{ secrets.DOCKER_HUB_USERNAME }}/test:test-${{ github.sha }}
build-args: VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
diff --git a/README.md b/README.md
index b5a58362d2..d9e0d5a418 100644
--- a/README.md
+++ b/README.md
@@ -1,173 +1,69 @@
-
-Stirling-PDF
+
+
+
-[](https://hub.docker.com/r/frooodle/s-pdf)
-[](https://discord.gg/HYmhKj45pU)
-[](https://scorecard.dev/viewer/?uri=github.com/Stirling-Tools/Stirling-PDF)
-[](https://github.com/Stirling-Tools/stirling-pdf)
+Stirling PDF - The Open-Source PDF Platform
-
-[](https://cloud.digitalocean.com/apps/new?repo=https://github.com/Stirling-Tools/Stirling-PDF/tree/digitalOcean&refcode=c3210994b1af)
+Stirling PDF is a powerful, open-source PDF editing platform. Run it as a personal desktop app, in the browser, or deploy it on your own servers with a private API. Edit, sign, redact, convert, and automate PDFs without sending documents to external services.
-[Stirling-PDF](https://www.stirlingpdf.com) is a robust, locally hosted web-based PDF manipulation tool using Docker. It enables you to carry out various operations on PDF files, including splitting, merging, converting, reorganizing, adding images, rotating, compressing, and more. This locally hosted web application has evolved to encompass a comprehensive set of features, addressing all your PDF requirements.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-All files and PDFs exist either exclusively on the client side, reside in server memory only during task execution, or temporarily reside in a file solely for the execution of the task. Any file downloaded by the user will have been deleted from the server by that point.
+
-Homepage: [https://stirlingpdf.com](https://stirlingpdf.com)
+## Key Capabilities
-All documentation available at [https://docs.stirlingpdf.com/](https://docs.stirlingpdf.com/)
+- **Everywhere you work** - Desktop client, browser UI, and self-hosted server with a private API.
+- **50+ PDF tools** - Edit, merge, split, sign, redact, convert, OCR, compress, and more.
+- **Automation & workflows** - No-code pipelines direct in UI with APIs to process millions of PDFs.
+- **Enterprise‑grade** - SSO, auditing, and flexible on‑prem deployments.
+- **Developer platform** - REST APIs available for nearly all tools to integrate into your existing systems.
+- **Global UI** - Interface available in 40+ languages.
-
+For a full feature list, see the docs: **https://docs.stirlingpdf.com**
-## Features
+## Quick Start
-- 50+ PDF Operations
-- Parallel file processing and downloads
-- Dark mode support
-- Custom download options
-- Custom 'Pipelines' to run multiple features in a automated queue
-- API for integration with external scripts
-- Optional Login and Authentication support (see [here](https://docs.stirlingpdf.com/Advanced%20Configuration/System%20and%20Security) for documentation)
-- Database Backup and Import (see [here](https://docs.stirlingpdf.com/Advanced%20Configuration/DATABASE) for documentation)
-- Enterprise features like SSO (see [here](https://docs.stirlingpdf.com/Advanced%20Configuration/Single%20Sign-On%20Configuration) for documentation)
+```bash
+docker run -p 8080:8080 docker.stirlingpdf.com/stirlingtools/stirling-pdf
+```
-## PDF Features
+Then open: http://localhost:8080
-### Page Operations
+For full installation options (including desktop and Kubernetes), see our [Documentation Guide](https://docs.stirlingpdf.com/#documentation-guide).
-- View and modify PDFs - View multi-page PDFs with custom viewing, sorting, and searching. Plus, on-page edit features like annotating, drawing, and adding text and images. (Using PDF.js with Joxit and Liberation fonts)
-- Full interactive GUI for merging/splitting/rotating/moving PDFs and their pages
-- Merge multiple PDFs into a single resultant file
-- Split PDFs into multiple files at specified page numbers or extract all pages as individual files
-- Reorganize PDF pages into different orders
-- Rotate PDFs in 90-degree increments
-- Remove pages
-- Multi-page layout (format PDFs into a multi-paged page)
-- Scale page contents size by set percentage
-- Adjust contrast
-- Crop PDF
-- Auto-split PDF (with physically scanned page dividers)
-- Extract page(s)
-- Convert PDF to a single page
-- Overlay PDFs on top of each other
-- PDF to a single page
-- Split PDF by sections
+## Resources
-### Conversion Operations
+- [**Documentation**](https://docs.stirlingpdf.com)
+- [**Homepage**](https://stirling.com)
+- [**API Docs**](https://registry.scalar.com/@stirlingpdf/apis/stirling-pdf-processing-api/)
+- [**Server Plan & Enterprise**](https://docs.stirlingpdf.com/Paid-Offerings)
-- Convert PDFs to and from images
-- Convert any common file to PDF (using LibreOffice)
-- Convert PDF to Word/PowerPoint/others (using LibreOffice)
-- Convert HTML to PDF
-- Convert PDF to XML
-- Convert PDF to CSV
-- URL to PDF
-- Markdown to PDF
+## Support
-### Security & Permissions
+- **Community** [Discord](https://discord.gg/HYmhKj45pU)
+- **Bug Reports**: [Github issues](https://github.com/Stirling-Tools/Stirling-PDF/issues)
-- Add and remove passwords
-- Change/set PDF permissions
-- Add watermark(s)
-- Certify/sign PDFs
-- Sanitize PDFs
-- Auto-redact text
+## Contributing
-### Other Operations
+We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
-- Add/generate/write signatures
-- Split by Size or PDF
-- Repair PDFs
-- Detect and remove blank pages
-- Compare two PDFs and show differences in text
-- Add images to PDFs
-- Compress PDFs to decrease their filesize (using qpdf)
-- Extract images from PDF
-- Remove images from PDF
-- Extract images from scans
-- Remove annotations
-- Add page numbers
-- Auto-rename files by detecting PDF header text
-- OCR on PDF (using Tesseract OCR)
-- PDF/A conversion (using LibreOffice)
-- Edit metadata
-- Flatten PDFs
-- Get all information on a PDF to view or export as JSON
-- Show/detect embedded JavaScript
+For development setup, see the [Developer Guide](DeveloperGuide.md).
+For adding translations, see the [Translation Guide](devGuide/HowToAddNewLanguage.md).
+## License
-# 📖 Get Started
-
-Visit our comprehensive documentation at [docs.stirlingpdf.com](https://docs.stirlingpdf.com) for:
-
-- Installation guides for all platforms
-- Configuration options
-- Feature documentation
-- API reference
-- Security setup
-- Enterprise features
-
-
-## Supported Languages
-
-Stirling-PDF currently supports 40 languages!
-
-| Language | Progress |
-| -------------------------------------------- | -------------------------------------- |
-| Arabic (العربية) (ar_AR) |  |
-| Azerbaijani (Azərbaycan Dili) (az_AZ) |  |
-| Basque (Euskara) (eu_ES) |  |
-| Bulgarian (Български) (bg_BG) |  |
-| Catalan (Català) (ca_CA) |  |
-| Croatian (Hrvatski) (hr_HR) |  |
-| Czech (Česky) (cs_CZ) |  |
-| Danish (Dansk) (da_DK) |  |
-| Dutch (Nederlands) (nl_NL) |  |
-| English (English) (en_GB) |  |
-| English (US) (en_US) |  |
-| French (Français) (fr_FR) |  |
-| German (Deutsch) (de_DE) |  |
-| Greek (Ελληνικά) (el_GR) |  |
-| Hindi (हिंदी) (hi_IN) |  |
-| Hungarian (Magyar) (hu_HU) |  |
-| Indonesian (Bahasa Indonesia) (id_ID) |  |
-| Irish (Gaeilge) (ga_IE) |  |
-| Italian (Italiano) (it_IT) |  |
-| Japanese (日本語) (ja_JP) |  |
-| Korean (한국어) (ko_KR) |  |
-| Norwegian (Norsk) (no_NB) |  |
-| Persian (فارسی) (fa_IR) |  |
-| Polish (Polski) (pl_PL) |  |
-| Portuguese (Português) (pt_PT) |  |
-| Portuguese Brazilian (Português) (pt_BR) |  |
-| Romanian (Română) (ro_RO) |  |
-| Russian (Русский) (ru_RU) |  |
-| Serbian Latin alphabet (Srpski) (sr_LATN_RS) |  |
-| Simplified Chinese (简体中文) (zh_CN) |  |
-| Slovakian (Slovensky) (sk_SK) |  |
-| Slovenian (Slovenščina) (sl_SI) |  |
-| Spanish (Español) (es_ES) |  |
-| Swedish (Svenska) (sv_SE) |  |
-| Thai (ไทย) (th_TH) |  |
-| Tibetan (བོད་ཡིག་) (bo_CN) |  |
-| Traditional Chinese (繁體中文) (zh_TW) |  |
-| Turkish (Türkçe) (tr_TR) |  |
-| Ukrainian (Українська) (uk_UA) |  |
-| Vietnamese (Tiếng Việt) (vi_VN) |  |
-| Malayalam (മലയാളം) (ml_IN) |  |
-
-## Stirling PDF Enterprise
-
-Stirling PDF offers an Enterprise edition of its software. This is the same great software but with added features, support and comforts.
-Check out our [Enterprise docs](https://docs.stirlingpdf.com/Pro)
-
-
-## 🤝 Looking to contribute?
-
-Join our community:
-- [Contribution Guidelines](CONTRIBUTING.md)
-- [Translation Guide (How to add custom languages)](devGuide/HowToAddNewLanguage.md)
-- [Developer Guide](devGuide/DeveloperGuide.md)
-- [Issue Tracker](https://github.com/Stirling-Tools/Stirling-PDF/issues)
-- [Discord Community](https://discord.gg/HYmhKj45pU)
+Stirling PDF is open-core. See [LICENSE](LICENSE) for details.
diff --git a/app/common/src/main/java/stirling/software/common/util/RequestUriUtils.java b/app/common/src/main/java/stirling/software/common/util/RequestUriUtils.java
index 2dd3d420a5..a0adec7de2 100644
--- a/app/common/src/main/java/stirling/software/common/util/RequestUriUtils.java
+++ b/app/common/src/main/java/stirling/software/common/util/RequestUriUtils.java
@@ -39,6 +39,7 @@ public class RequestUriUtils {
// Specific static files bundled with the frontend
if (normalizedUri.equals("/robots.txt")
|| normalizedUri.equals("/favicon.ico")
+ || normalizedUri.equals("/manifest.json")
|| normalizedUri.equals("/site.webmanifest")
|| normalizedUri.equals("/manifest-classic.json")
|| normalizedUri.equals("/index.html")) {
diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/web/ReactRoutingController.java b/app/core/src/main/java/stirling/software/SPDF/controller/web/ReactRoutingController.java
index daa6233ce8..424621b8ad 100644
--- a/app/core/src/main/java/stirling/software/SPDF/controller/web/ReactRoutingController.java
+++ b/app/core/src/main/java/stirling/software/SPDF/controller/web/ReactRoutingController.java
@@ -1,20 +1,89 @@
package stirling.software.SPDF.controller.web;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+
+import jakarta.annotation.PostConstruct;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
+import jakarta.servlet.http.HttpServletRequest;
+
@Controller
public class ReactRoutingController {
- @GetMapping(
- "/{path:^(?!api|static|robots\\.txt|favicon\\.ico|pipeline|pdfjs|pdfjs-legacy|fonts|images|files|css|js)[^\\.]*$}")
- public String forwardRootPaths() {
- return "forward:/index.html";
+ @Value("${server.servlet.context-path:/}")
+ private String contextPath;
+
+ private String cachedIndexHtml;
+ private boolean indexHtmlExists = false;
+
+ @PostConstruct
+ public void init() {
+ // Only cache if index.html exists (production builds)
+ ClassPathResource resource = new ClassPathResource("static/index.html");
+ if (resource.exists()) {
+ try {
+ this.cachedIndexHtml = processIndexHtml();
+ this.indexHtmlExists = true;
+ } catch (IOException e) {
+ // Failed to cache, will process on each request
+ this.indexHtmlExists = false;
+ }
+ }
+ }
+
+ private String processIndexHtml() throws IOException {
+ ClassPathResource resource = new ClassPathResource("static/index.html");
+
+ try (InputStream inputStream = resource.getInputStream()) {
+ String html = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
+
+ // Replace %BASE_URL% with the actual context path for base href
+ String baseUrl = contextPath.endsWith("/") ? contextPath : contextPath + "/";
+ html = html.replace("%BASE_URL%", baseUrl);
+ // Also rewrite any existing tag (Vite may have baked one in)
+ html =
+ html.replaceFirst(
+ " ",
+ " ");
+
+ // Inject context path as a global variable for API calls
+ String contextPathScript =
+ "";
+ html = html.replace("", contextPathScript + "");
+
+ return html;
+ }
+ }
+
+ @GetMapping(value = {"/", "/index.html"}, produces = MediaType.TEXT_HTML_VALUE)
+ public ResponseEntity serveIndexHtml(HttpServletRequest request)
+ throws IOException {
+ if (indexHtmlExists && cachedIndexHtml != null) {
+ return ResponseEntity.ok().contentType(MediaType.TEXT_HTML).body(cachedIndexHtml);
+ }
+ // Fallback: process on each request (dev mode or cache failed)
+ return ResponseEntity.ok().contentType(MediaType.TEXT_HTML).body(processIndexHtml());
}
@GetMapping(
- "/{path:^(?!api|static|pipeline|pdfjs|pdfjs-legacy|fonts|images|files|css|js)[^\\.]*}/{subpath:^(?!.*\\.).*$}")
- public String forwardNestedPaths() {
- return "forward:/index.html";
+ "/{path:^(?!api|static|robots\\.txt|favicon\\.ico|manifest.*\\.json|pipeline|pdfjs|pdfjs-legacy|fonts|images|files|css|js|assets|locales|modern-logo|classic-logo|Login|og_images|samples)[^\\.]*$}")
+ public ResponseEntity forwardRootPaths(HttpServletRequest request)
+ throws IOException {
+ return serveIndexHtml(request);
+ }
+
+ @GetMapping(
+ "/{path:^(?!api|static|pipeline|pdfjs|pdfjs-legacy|fonts|images|files|css|js|assets|locales|modern-logo|classic-logo|Login|og_images|samples)[^\\.]*}/{subpath:^(?!.*\\.).*$}")
+ public ResponseEntity forwardNestedPaths(HttpServletRequest request)
+ throws IOException {
+ return serveIndexHtml(request);
}
}
diff --git a/app/core/src/main/java/stirling/software/SPDF/service/SharedSignatureService.java b/app/core/src/main/java/stirling/software/SPDF/service/SharedSignatureService.java
index 6c349581db..7043118a46 100644
--- a/app/core/src/main/java/stirling/software/SPDF/service/SharedSignatureService.java
+++ b/app/core/src/main/java/stirling/software/SPDF/service/SharedSignatureService.java
@@ -179,7 +179,7 @@ public class SharedSignatureService {
StandardOpenOption.TRUNCATE_EXISTING);
// Store reference to image file
- response.setDataUrl("/api/v1/general/sign/" + imageFileName);
+ response.setDataUrl("/api/v1/general/signatures/" + imageFileName);
}
log.info("Saved signature {} for user {}", request.getId(), username);
@@ -207,7 +207,7 @@ public class SharedSignatureService {
sig.setLabel(id); // Use ID as label
sig.setType("image"); // Default type
sig.setScope("personal");
- sig.setDataUrl("/api/v1/general/sign/" + fileName);
+ sig.setDataUrl("/api/v1/general/signatures/" + fileName);
sig.setCreatedAt(
Files.getLastModifiedTime(path).toMillis());
sig.setUpdatedAt(
@@ -238,7 +238,7 @@ public class SharedSignatureService {
sig.setLabel(id); // Use ID as label
sig.setType("image"); // Default type
sig.setScope("shared");
- sig.setDataUrl("/api/v1/general/sign/" + fileName);
+ sig.setDataUrl("/api/v1/general/signatures/" + fileName);
sig.setCreatedAt(
Files.getLastModifiedTime(path).toMillis());
sig.setUpdatedAt(
diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/SignatureController.java b/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/SignatureController.java
index f42b23a972..a073c21371 100644
--- a/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/SignatureController.java
+++ b/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/SignatureController.java
@@ -1,7 +1,12 @@
package stirling.software.proprietary.controller.api;
import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
import java.util.List;
+import java.util.Map;
+import java.util.stream.Stream;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
@@ -18,6 +23,7 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import stirling.software.common.annotations.api.UserApi;
+import stirling.software.common.configuration.InstallationPathConfig;
import stirling.software.proprietary.model.api.signature.SavedSignatureRequest;
import stirling.software.proprietary.model.api.signature.SavedSignatureResponse;
import stirling.software.proprietary.security.service.UserService;
@@ -38,6 +44,7 @@ public class SignatureController {
private final SignatureService signatureService;
private final UserService userService;
+ private static final String ALL_USERS_FOLDER = "ALL_USERS";
/**
* Save a new signature for the authenticated user. Enforces storage limits and authentication
@@ -84,19 +91,105 @@ public class SignatureController {
}
/**
- * Delete a signature owned by the authenticated user. Users can only delete their own personal
- * signatures, not shared ones.
+ * Update a signature label. Users can update labels for their own personal signatures and for
+ * shared signatures.
+ */
+ @PostMapping("/{signatureId}/label")
+ @PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
+ public ResponseEntity updateSignatureLabel(
+ @PathVariable String signatureId, @RequestBody Map body) {
+ try {
+ String username = userService.getCurrentUsername();
+ String newLabel = body.get("label");
+
+ if (newLabel == null || newLabel.trim().isEmpty()) {
+ log.warn("Invalid label update request");
+ return ResponseEntity.badRequest().build();
+ }
+
+ signatureService.updateSignatureLabel(username, signatureId, newLabel);
+ log.info("User {} updated label for signature {}", username, signatureId);
+ return ResponseEntity.noContent().build();
+ } catch (IOException e) {
+ log.warn("Failed to update signature label: {}", e.getMessage());
+ return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
+ }
+ }
+
+ /**
+ * Delete a signature owned by the authenticated user. Users can delete their own personal
+ * signatures. Admins can also delete shared signatures.
*/
@DeleteMapping("/{signatureId}")
+ @PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
public ResponseEntity deleteSignature(@PathVariable String signatureId) {
try {
String username = userService.getCurrentUsername();
- signatureService.deleteSignature(username, signatureId);
- log.info("User {} deleted signature {}", username, signatureId);
- return ResponseEntity.noContent().build();
+ boolean isAdmin = userService.isCurrentUserAdmin();
+
+ // Validate filename to prevent path traversal
+ if (signatureId.contains("..")
+ || signatureId.contains("/")
+ || signatureId.contains("\\")) {
+ log.warn("Invalid signature ID: {}", signatureId);
+ return ResponseEntity.badRequest().build();
+ }
+
+ // Try to delete from personal folder first
+ try {
+ signatureService.deleteSignature(username, signatureId);
+ log.info("User {} deleted personal signature {}", username, signatureId);
+ return ResponseEntity.noContent().build();
+ } catch (IOException e) {
+ // If not found in personal folder, check if it's in shared folder
+ if (isAdmin) {
+ // Admin can delete from shared folder
+ if (deleteFromSharedFolder(signatureId)) {
+ log.info("Admin {} deleted shared signature {}", username, signatureId);
+ return ResponseEntity.noContent().build();
+ }
+ }
+ // If not admin or not found in shared folder either, return 404
+ throw e;
+ }
} catch (IOException e) {
log.warn("Failed to delete signature {} for user: {}", signatureId, e.getMessage());
return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
}
}
+
+ /**
+ * Delete a signature from the shared (ALL_USERS) folder. Only admins should call this method.
+ */
+ private boolean deleteFromSharedFolder(String signatureId) throws IOException {
+ String signatureBasePath = InstallationPathConfig.getSignaturesPath();
+ Path sharedFolder = Paths.get(signatureBasePath, ALL_USERS_FOLDER);
+ boolean deleted = false;
+
+ if (Files.exists(sharedFolder)) {
+ try (Stream stream = Files.list(sharedFolder)) {
+ List matchingFiles =
+ stream.filter(
+ path ->
+ path.getFileName()
+ .toString()
+ .startsWith(signatureId + "."))
+ .toList();
+ for (Path file : matchingFiles) {
+ Files.delete(file);
+ deleted = true;
+ log.info("Deleted shared signature file: {}", file);
+ }
+ }
+
+ // Also delete metadata file if it exists
+ Path metadataPath = sharedFolder.resolve(signatureId + ".json");
+ if (Files.exists(metadataPath)) {
+ Files.delete(metadataPath);
+ log.info("Deleted shared signature metadata: {}", metadataPath);
+ }
+ }
+
+ return deleted;
+ }
}
diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java
index e683fa8dc5..a3a5eee4f0 100644
--- a/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java
+++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java
@@ -324,10 +324,14 @@ public class SecurityConfiguration {
.authenticated());
// Handle User/Password Logins
if (securityProperties.isUserPass()) {
+ // v2: Authentication is handled via API (/api/v1/auth/login), not form login
+ // We configure form login to handle Spring Security redirects,
+ // but use /perform_login as the processing URL so /login remains a React route
http.formLogin(
formLogin ->
formLogin
- .loginPage("/login")
+ .loginPage("/login") // Redirect here when unauthenticated
+ .loginProcessingUrl("/perform_login") // Process form posts here (not /login)
.successHandler(
new CustomAuthenticationSuccessHandler(
loginAttemptService,
diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/service/SignatureService.java b/app/proprietary/src/main/java/stirling/software/proprietary/service/SignatureService.java
index 6f849ebfe6..63ee285123 100644
--- a/app/proprietary/src/main/java/stirling/software/proprietary/service/SignatureService.java
+++ b/app/proprietary/src/main/java/stirling/software/proprietary/service/SignatureService.java
@@ -2,6 +2,7 @@ package stirling.software.proprietary.service;
import java.io.FileNotFoundException;
import java.io.IOException;
+import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@@ -13,6 +14,8 @@ import java.util.stream.Stream;
import org.springframework.stereotype.Service;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
import lombok.extern.slf4j.Slf4j;
import stirling.software.common.configuration.InstallationPathConfig;
@@ -31,6 +34,7 @@ public class SignatureService implements PersonalSignatureServiceInterface {
private final String SIGNATURE_BASE_PATH;
private final String ALL_USERS_FOLDER = "ALL_USERS";
+ private final ObjectMapper objectMapper = new ObjectMapper();
// Storage limits per user
private static final int MAX_SIGNATURES_PER_USER = 20;
@@ -88,6 +92,14 @@ public class SignatureService implements PersonalSignatureServiceInterface {
response.setCreatedAt(timestamp);
response.setUpdatedAt(timestamp);
+ // Copy text signature properties if present
+ if ("text".equals(request.getType())) {
+ response.setSignerName(request.getSignerName());
+ response.setFontFamily(request.getFontFamily());
+ response.setFontSize(request.getFontSize());
+ response.setTextColor(request.getTextColor());
+ }
+
// Extract and save image data
String dataUrl = request.getDataUrl();
if (dataUrl != null && dataUrl.startsWith("data:image/")) {
@@ -133,6 +145,19 @@ public class SignatureService implements PersonalSignatureServiceInterface {
response.setDataUrl("/api/v1/general/signatures/" + imageFileName);
}
+ // Save metadata JSON file
+ String metadataFileName = request.getId() + ".json";
+ Path metadataPath = targetFolder.resolve(metadataFileName);
+ verifyPathWithinDirectory(metadataPath, targetFolder);
+
+ String metadataJson = objectMapper.writeValueAsString(response);
+ Files.writeString(
+ metadataPath,
+ metadataJson,
+ StandardCharsets.UTF_8,
+ StandardOpenOption.CREATE,
+ StandardOpenOption.TRUNCATE_EXISTING);
+
log.info("Saved signature {} for user {} (scope: {})", request.getId(), username, scope);
return response;
}
@@ -179,6 +204,13 @@ public class SignatureService implements PersonalSignatureServiceInterface {
log.info("Deleted signature file: {}", file);
}
}
+
+ // Also delete metadata file if it exists
+ Path metadataPath = personalFolder.resolve(signatureId + ".json");
+ if (Files.exists(metadataPath)) {
+ Files.delete(metadataPath);
+ log.info("Deleted signature metadata: {}", metadataPath);
+ }
}
if (!deleted) {
@@ -186,6 +218,50 @@ public class SignatureService implements PersonalSignatureServiceInterface {
}
}
+ /** Update a signature label. */
+ public void updateSignatureLabel(String username, String signatureId, String newLabel)
+ throws IOException {
+ validateFileName(signatureId);
+
+ // Try personal folder first
+ Path personalFolder = Paths.get(SIGNATURE_BASE_PATH, username);
+ Path metadataPath = personalFolder.resolve(signatureId + ".json");
+
+ if (Files.exists(metadataPath)) {
+ updateMetadataLabel(metadataPath, newLabel);
+ log.info("Updated label for personal signature {} (user: {})", signatureId, username);
+ return;
+ }
+
+ // If not found in personal, try shared folder
+ Path sharedFolder = Paths.get(SIGNATURE_BASE_PATH, ALL_USERS_FOLDER);
+ Path sharedMetadataPath = sharedFolder.resolve(signatureId + ".json");
+
+ if (Files.exists(sharedMetadataPath)) {
+ updateMetadataLabel(sharedMetadataPath, newLabel);
+ log.info("Updated label for shared signature {}", signatureId);
+ return;
+ }
+
+ throw new FileNotFoundException("Signature metadata not found");
+ }
+
+ private void updateMetadataLabel(Path metadataPath, String newLabel) throws IOException {
+ String metadataJson = Files.readString(metadataPath, StandardCharsets.UTF_8);
+ SavedSignatureResponse sig =
+ objectMapper.readValue(metadataJson, SavedSignatureResponse.class);
+ sig.setLabel(newLabel);
+ sig.setUpdatedAt(System.currentTimeMillis());
+
+ String updatedJson = objectMapper.writeValueAsString(sig);
+ Files.writeString(
+ metadataPath,
+ updatedJson,
+ StandardCharsets.UTF_8,
+ StandardOpenOption.CREATE,
+ StandardOpenOption.TRUNCATE_EXISTING);
+ }
+
// Private helper methods
private void enforceStorageLimits(String username, String dataUrlToAdd) throws IOException {
@@ -245,16 +321,31 @@ public class SignatureService implements PersonalSignatureServiceInterface {
String fileName = path.getFileName().toString();
String id = fileName.substring(0, fileName.lastIndexOf('.'));
- SavedSignatureResponse sig = new SavedSignatureResponse();
- sig.setId(id);
- sig.setLabel(id);
- sig.setType("image");
- sig.setScope(scope);
- sig.setCreatedAt(Files.getLastModifiedTime(path).toMillis());
- sig.setUpdatedAt(Files.getLastModifiedTime(path).toMillis());
+ // Try to load metadata from JSON file
+ Path metadataPath = folder.resolve(id + ".json");
+ SavedSignatureResponse sig;
- // Set unified URL path (works for both personal and shared)
- sig.setDataUrl("/api/v1/general/signatures/" + fileName);
+ if (Files.exists(metadataPath)) {
+ // Load from metadata file
+ String metadataJson =
+ Files.readString(
+ metadataPath, StandardCharsets.UTF_8);
+ sig =
+ objectMapper.readValue(
+ metadataJson, SavedSignatureResponse.class);
+ } else {
+ // Fallback for old signatures without metadata
+ sig = new SavedSignatureResponse();
+ sig.setId(id);
+ sig.setLabel(id);
+ sig.setType("image");
+ sig.setScope(scope);
+ sig.setCreatedAt(
+ Files.getLastModifiedTime(path).toMillis());
+ sig.setUpdatedAt(
+ Files.getLastModifiedTime(path).toMillis());
+ sig.setDataUrl("/api/v1/general/signatures/" + fileName);
+ }
signatures.add(sig);
} catch (IOException e) {
diff --git a/docker/embedded/Dockerfile b/docker/embedded/Dockerfile
new file mode 100644
index 0000000000..a38aee9b4a
--- /dev/null
+++ b/docker/embedded/Dockerfile
@@ -0,0 +1,138 @@
+# Stirling-PDF Dockerfile - Full version with embedded frontend
+# Single JAR contains both frontend and backend
+
+# Stage 1: Build application with embedded frontend
+FROM gradle:8.14-jdk21 AS build
+
+# Install Node.js and npm for frontend build
+RUN apt-get update && apt-get install -y \
+ curl \
+ && curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
+ && apt-get install -y nodejs \
+ && npm --version \
+ && node --version \
+ && apt-get clean \
+ && rm -rf /var/lib/apt/lists/*
+
+# Copy gradle files for dependency resolution
+COPY build.gradle .
+COPY settings.gradle .
+COPY gradlew .
+COPY gradle gradle/
+COPY app/core/build.gradle core/.
+COPY app/common/build.gradle common/.
+COPY app/proprietary/build.gradle proprietary/.
+RUN ./gradlew build -x spotlessApply -x spotlessCheck -x test -x sonarqube || return 0
+
+# Set working directory
+WORKDIR /app
+
+# Copy entire project
+COPY . .
+
+# Build JAR with embedded frontend (includes security features controlled at runtime)
+RUN DISABLE_ADDITIONAL_FEATURES=false \
+ STIRLING_PDF_DESKTOP_UI=false \
+ ./gradlew clean build -PbuildWithFrontend=true -x spotlessApply -x spotlessCheck -x test -x sonarqube
+
+# Stage 2: Runtime image
+FROM alpine:3.22.1
+
+ARG VERSION_TAG
+
+# Labels
+LABEL org.opencontainers.image.title="Stirling-PDF"
+LABEL org.opencontainers.image.description="Stirling-PDF with embedded frontend - Full version"
+LABEL org.opencontainers.image.source="https://github.com/Stirling-Tools/Stirling-PDF"
+LABEL org.opencontainers.image.licenses="MIT"
+LABEL org.opencontainers.image.vendor="Stirling-Tools"
+LABEL org.opencontainers.image.url="https://www.stirlingpdf.com"
+LABEL org.opencontainers.image.documentation="https://docs.stirlingpdf.com"
+LABEL maintainer="Stirling-Tools"
+LABEL org.opencontainers.image.authors="Stirling-Tools"
+LABEL org.opencontainers.image.version="${VERSION_TAG}"
+LABEL org.opencontainers.image.keywords="PDF, manipulation, API, Spring Boot, React"
+
+# Copy scripts and fonts
+COPY scripts /scripts
+COPY app/core/src/main/resources/static/fonts/*.ttf /usr/share/fonts/opentype/noto/
+
+# Copy built JAR from build stage
+COPY --from=build /app/app/core/build/libs/*.jar /app.jar
+COPY --from=build /app/build/libs/restart-helper.jar /restart-helper.jar
+
+# Environment Variables
+ENV VERSION_TAG=$VERSION_TAG \
+ JAVA_BASE_OPTS="-XX:+UnlockExperimentalVMOptions -XX:MaxRAMPercentage=75 -XX:InitiatingHeapOccupancyPercent=20 -XX:+G1PeriodicGCInvokesConcurrent -XX:G1PeriodicGCInterval=10000 -XX:+UseStringDeduplication -XX:G1PeriodicGCSystemLoadThreshold=70" \
+ JAVA_CUSTOM_OPTS="" \
+ HOME=/home/stirlingpdfuser \
+ PUID=1000 \
+ PGID=1000 \
+ UMASK=022 \
+ PYTHONPATH=/usr/lib/libreoffice/program:/opt/venv/lib/python3.12/site-packages \
+ UNO_PATH=/usr/lib/libreoffice/program \
+ URE_BOOTSTRAP=file:///usr/lib/libreoffice/program/fundamentalrc \
+ PATH=$PATH:/opt/venv/bin \
+ STIRLING_TEMPFILES_DIRECTORY=/tmp/stirling-pdf \
+ TMPDIR=/tmp/stirling-pdf \
+ TEMP=/tmp/stirling-pdf \
+ TMP=/tmp/stirling-pdf
+
+# Install all dependencies
+RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/apk/repositories && \
+ echo "@community https://dl-cdn.alpinelinux.org/alpine/edge/community" | tee -a /etc/apk/repositories && \
+ echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/testing" | tee -a /etc/apk/repositories && \
+ apk upgrade --no-cache -a && \
+ apk add --no-cache \
+ ca-certificates \
+ tzdata \
+ tini \
+ bash \
+ curl \
+ shadow \
+ su-exec \
+ openssl \
+ openssl-dev \
+ openjdk21-jre \
+ # Doc conversion
+ gcompat \
+ libc6-compat \
+ libreoffice \
+ ghostscript \
+ fontforge \
+ # pdftohtml
+ poppler-utils \
+ # OCR MY PDF
+ unpaper \
+ tesseract-ocr-data-eng \
+ tesseract-ocr-data-chi_sim \
+ tesseract-ocr-data-deu \
+ tesseract-ocr-data-fra \
+ tesseract-ocr-data-por \
+ ocrmypdf \
+ # CV
+ py3-opencv \
+ python3 \
+ py3-pip \
+ py3-pillow@testing \
+ py3-pdf2image@testing && \
+ python3 -m venv /opt/venv && \
+ /opt/venv/bin/pip install --upgrade pip setuptools && \
+ /opt/venv/bin/pip install --no-cache-dir --upgrade unoserver weasyprint && \
+ ln -s /usr/lib/libreoffice/program/uno.py /opt/venv/lib/python3.12/site-packages/ && \
+ ln -s /usr/lib/libreoffice/program/unohelper.py /opt/venv/lib/python3.12/site-packages/ && \
+ ln -s /usr/lib/libreoffice/program /opt/venv/lib/python3.12/site-packages/LibreOffice && \
+ mv /usr/share/tessdata /usr/share/tessdata-original && \
+ mkdir -p $HOME /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders /tmp/stirling-pdf && \
+ fc-cache -f -v && \
+ chmod +x /scripts/* && \
+ # User permissions
+ addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \
+ chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /pipeline /usr/share/fonts/opentype/noto /configs /customFiles /tmp/stirling-pdf && \
+ chown stirlingpdfuser:stirlingpdfgroup /app.jar /restart-helper.jar
+
+EXPOSE 8080/tcp
+
+# Set user and run command
+ENTRYPOINT ["tini", "--", "/scripts/init.sh"]
+CMD ["sh", "-c", "java -Dfile.encoding=UTF-8 -Djava.io.tmpdir=/tmp/stirling-pdf -jar /app.jar & /opt/venv/bin/unoserver --port 2003 --interface 127.0.0.1"]
diff --git a/docker/embedded/Dockerfile.fat b/docker/embedded/Dockerfile.fat
new file mode 100644
index 0000000000..67e648aee0
--- /dev/null
+++ b/docker/embedded/Dockerfile.fat
@@ -0,0 +1,142 @@
+# Stirling-PDF Dockerfile - Fat version with embedded frontend
+# Single JAR contains both frontend and backend with extra fonts for air-gapped environments
+
+# Stage 1: Build application with embedded frontend
+FROM gradle:8.14-jdk21 AS build
+
+# Install Node.js and npm for frontend build
+RUN apt-get update && apt-get install -y \
+ curl \
+ && curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
+ && apt-get install -y nodejs \
+ && npm --version \
+ && node --version \
+ && apt-get clean \
+ && rm -rf /var/lib/apt/lists/*
+
+# Copy gradle files for dependency resolution
+COPY build.gradle .
+COPY settings.gradle .
+COPY gradlew .
+COPY gradle gradle/
+COPY app/core/build.gradle core/.
+COPY app/common/build.gradle common/.
+COPY app/proprietary/build.gradle proprietary/.
+RUN ./gradlew build -x spotlessApply -x spotlessCheck -x test -x sonarqube || return 0
+
+# Set working directory
+WORKDIR /app
+
+# Copy entire project
+COPY . .
+
+# Build JAR with embedded frontend (includes security features controlled at runtime)
+RUN DISABLE_ADDITIONAL_FEATURES=false \
+ STIRLING_PDF_DESKTOP_UI=false \
+ ./gradlew clean build -PbuildWithFrontend=true -x spotlessApply -x spotlessCheck -x test -x sonarqube
+
+# Stage 2: Runtime image
+FROM alpine:3.22.1
+
+ARG VERSION_TAG
+
+# Labels
+LABEL org.opencontainers.image.title="Stirling-PDF Fat"
+LABEL org.opencontainers.image.description="Stirling-PDF with embedded frontend - Fat version with extra fonts for air-gapped environments"
+LABEL org.opencontainers.image.source="https://github.com/Stirling-Tools/Stirling-PDF"
+LABEL org.opencontainers.image.licenses="MIT"
+LABEL org.opencontainers.image.vendor="Stirling-Tools"
+LABEL org.opencontainers.image.url="https://www.stirlingpdf.com"
+LABEL org.opencontainers.image.documentation="https://docs.stirlingpdf.com"
+LABEL maintainer="Stirling-Tools"
+LABEL org.opencontainers.image.authors="Stirling-Tools"
+LABEL org.opencontainers.image.version="${VERSION_TAG}"
+LABEL org.opencontainers.image.keywords="PDF, manipulation, fat, air-gapped, API, Spring Boot, React"
+
+# Copy scripts and fonts
+COPY scripts /scripts
+COPY app/core/src/main/resources/static/fonts/*.ttf /usr/share/fonts/opentype/noto/
+
+# Copy built JAR from build stage
+COPY --from=build /app/app/core/build/libs/*.jar /app.jar
+COPY --from=build /app/build/libs/restart-helper.jar /restart-helper.jar
+
+# Environment Variables
+ENV VERSION_TAG=$VERSION_TAG \
+ JAVA_BASE_OPTS="-XX:+UnlockExperimentalVMOptions -XX:MaxRAMPercentage=75 -XX:InitiatingHeapOccupancyPercent=20 -XX:+G1PeriodicGCInvokesConcurrent -XX:G1PeriodicGCInterval=10000 -XX:+UseStringDeduplication -XX:G1PeriodicGCSystemLoadThreshold=70" \
+ JAVA_CUSTOM_OPTS="" \
+ HOME=/home/stirlingpdfuser \
+ PUID=1000 \
+ PGID=1000 \
+ UMASK=022 \
+ FAT_DOCKER=true \
+ INSTALL_BOOK_AND_ADVANCED_HTML_OPS=false \
+ PYTHONPATH=/usr/lib/libreoffice/program:/opt/venv/lib/python3.12/site-packages \
+ UNO_PATH=/usr/lib/libreoffice/program \
+ URE_BOOTSTRAP=file:///usr/lib/libreoffice/program/fundamentalrc \
+ PATH=$PATH:/opt/venv/bin \
+ STIRLING_TEMPFILES_DIRECTORY=/tmp/stirling-pdf \
+ TMPDIR=/tmp/stirling-pdf \
+ TEMP=/tmp/stirling-pdf \
+ TMP=/tmp/stirling-pdf
+
+# Install all dependencies plus extra fonts for air-gapped environments
+RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/apk/repositories && \
+ echo "@community https://dl-cdn.alpinelinux.org/alpine/edge/community" | tee -a /etc/apk/repositories && \
+ echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/testing" | tee -a /etc/apk/repositories && \
+ apk upgrade --no-cache -a && \
+ apk add --no-cache \
+ ca-certificates \
+ tzdata \
+ tini \
+ bash \
+ curl \
+ shadow \
+ su-exec \
+ openssl \
+ openssl-dev \
+ openjdk21-jre \
+ # Doc conversion
+ gcompat \
+ libc6-compat \
+ libreoffice \
+ ghostscript \
+ fontforge \
+ # pdftohtml
+ poppler-utils \
+ # OCR MY PDF
+ unpaper \
+ tesseract-ocr-data-eng \
+ tesseract-ocr-data-chi_sim \
+ tesseract-ocr-data-deu \
+ tesseract-ocr-data-fra \
+ tesseract-ocr-data-por \
+ ocrmypdf \
+ # Extra fonts for fat version
+ font-terminus font-dejavu font-noto font-noto-cjk font-awesome font-noto-extra font-liberation font-linux-libertine \
+ # CV
+ py3-opencv \
+ python3 \
+ py3-pip \
+ py3-pillow@testing \
+ py3-pdf2image@testing && \
+ python3 -m venv /opt/venv && \
+ /opt/venv/bin/pip install --upgrade pip setuptools && \
+ /opt/venv/bin/pip install --no-cache-dir --upgrade unoserver weasyprint && \
+ ln -s /usr/lib/libreoffice/program/uno.py /opt/venv/lib/python3.12/site-packages/ && \
+ ln -s /usr/lib/libreoffice/program/unohelper.py /opt/venv/lib/python3.12/site-packages/ && \
+ ln -s /usr/lib/libreoffice/program /opt/venv/lib/python3.12/site-packages/LibreOffice && \
+ mv /usr/share/tessdata /usr/share/tessdata-original && \
+ mkdir -p $HOME /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders /tmp/stirling-pdf && \
+ fc-cache -f -v && \
+ chmod +x /scripts/* && \
+ # User permissions
+ addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \
+ chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /pipeline /usr/share/fonts/opentype/noto /configs /customFiles /tmp/stirling-pdf && \
+ chown stirlingpdfuser:stirlingpdfgroup /app.jar /restart-helper.jar
+
+EXPOSE 8080/tcp
+
+# Set user and run command
+ENTRYPOINT ["tini", "--", "/scripts/init.sh"]
+CMD ["sh", "-c", "java -Dfile.encoding=UTF-8 -Djava.io.tmpdir=/tmp/stirling-pdf -jar /app.jar & /opt/venv/bin/unoserver --port 2003 --interface 127.0.0.1"]
diff --git a/docker/embedded/Dockerfile.ultra-lite b/docker/embedded/Dockerfile.ultra-lite
new file mode 100644
index 0000000000..a39a522ebc
--- /dev/null
+++ b/docker/embedded/Dockerfile.ultra-lite
@@ -0,0 +1,104 @@
+# Stirling-PDF Dockerfile - Ultra-lite version with embedded frontend
+# Single JAR contains both frontend and backend with minimal dependencies
+
+# Stage 1: Build application with embedded frontend
+FROM gradle:8.14-jdk21 AS build
+
+# Install Node.js and npm for frontend build
+RUN apt-get update && apt-get install -y \
+ curl \
+ && curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
+ && apt-get install -y nodejs \
+ && npm --version \
+ && node --version \
+ && apt-get clean \
+ && rm -rf /var/lib/apt/lists/*
+
+# Copy gradle files for dependency resolution
+COPY build.gradle .
+COPY settings.gradle .
+COPY gradlew .
+COPY gradle gradle/
+COPY app/core/build.gradle core/.
+COPY app/common/build.gradle common/.
+COPY app/proprietary/build.gradle proprietary/.
+RUN ./gradlew build -x spotlessApply -x spotlessCheck -x test -x sonarqube || return 0
+
+# Set working directory
+WORKDIR /app
+
+# Copy entire project
+COPY . .
+
+# Build ultra-lite JAR with embedded frontend (minimal features)
+RUN DISABLE_ADDITIONAL_FEATURES=true \
+ STIRLING_PDF_DESKTOP_UI=false \
+ ./gradlew clean build -PbuildWithFrontend=true -x spotlessApply -x spotlessCheck -x test -x sonarqube
+
+# Stage 2: Runtime image
+FROM alpine:3.22.1
+
+ARG VERSION_TAG
+
+# Labels
+LABEL org.opencontainers.image.title="Stirling-PDF Ultra-Lite"
+LABEL org.opencontainers.image.description="Stirling-PDF with embedded frontend - Ultra-lite version with minimal dependencies"
+LABEL org.opencontainers.image.source="https://github.com/Stirling-Tools/Stirling-PDF"
+LABEL org.opencontainers.image.licenses="MIT"
+LABEL org.opencontainers.image.vendor="Stirling-Tools"
+LABEL org.opencontainers.image.url="https://www.stirlingpdf.com"
+LABEL org.opencontainers.image.documentation="https://docs.stirlingpdf.com"
+LABEL maintainer="Stirling-Tools"
+LABEL org.opencontainers.image.authors="Stirling-Tools"
+LABEL org.opencontainers.image.version="${VERSION_TAG}"
+LABEL org.opencontainers.image.keywords="PDF, manipulation, ultra-lite, API, Spring Boot, React"
+
+# Copy scripts
+COPY scripts/init-without-ocr.sh /scripts/init-without-ocr.sh
+COPY scripts/installFonts.sh /scripts/installFonts.sh
+
+# Copy built JAR from build stage
+COPY --from=build /app/app/core/build/libs/*.jar /app.jar
+COPY --from=build /app/build/libs/restart-helper.jar /restart-helper.jar
+
+# Environment Variables
+ENV VERSION_TAG=$VERSION_TAG \
+ JAVA_BASE_OPTS="-XX:+UnlockExperimentalVMOptions -XX:MaxRAMPercentage=75 -XX:InitiatingHeapOccupancyPercent=20 -XX:+G1PeriodicGCInvokesConcurrent -XX:G1PeriodicGCInterval=10000 -XX:+UseStringDeduplication -XX:G1PeriodicGCSystemLoadThreshold=70" \
+ JAVA_CUSTOM_OPTS="" \
+ HOME=/home/stirlingpdfuser \
+ PUID=1000 \
+ PGID=1000 \
+ UMASK=022 \
+ STIRLING_TEMPFILES_DIRECTORY=/tmp/stirling-pdf \
+ TMPDIR=/tmp/stirling-pdf \
+ TEMP=/tmp/stirling-pdf \
+ TMP=/tmp/stirling-pdf \
+ ENDPOINTS_GROUPS_TO_REMOVE=CLI
+
+# Install minimal dependencies
+RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/apk/repositories && \
+ echo "@community https://dl-cdn.alpinelinux.org/alpine/edge/community" | tee -a /etc/apk/repositories && \
+ echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/testing" | tee -a /etc/apk/repositories && \
+ apk upgrade --no-cache -a && \
+ apk add --no-cache \
+ ca-certificates \
+ tzdata \
+ tini \
+ bash \
+ curl \
+ shadow \
+ su-exec \
+ openjdk21-jre && \
+ mkdir -p $HOME /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders /tmp/stirling-pdf && \
+ mkdir -p /usr/share/fonts/opentype/noto && \
+ chmod +x /scripts/*.sh && \
+ # User permissions
+ addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \
+ chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /configs /customFiles /pipeline /tmp/stirling-pdf && \
+ chown stirlingpdfuser:stirlingpdfgroup /app.jar /restart-helper.jar
+
+EXPOSE 8080/tcp
+
+# Set user and run command
+ENTRYPOINT ["tini", "--", "/scripts/init-without-ocr.sh"]
+CMD ["java", "-Dfile.encoding=UTF-8", "-Djava.io.tmpdir=/tmp/stirling-pdf", "-jar", "/app.jar"]
diff --git a/docker/frontend/nginx.conf b/docker/frontend/nginx.conf
index 3be5ec9005..ef74321efa 100644
--- a/docker/frontend/nginx.conf
+++ b/docker/frontend/nginx.conf
@@ -103,8 +103,8 @@ http {
add_header Cache-Control "public, immutable";
}
- # Cache static assets
- location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
+ # Cache static assets (but not API endpoints)
+ location ~* ^(?!/api/).*\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
diff --git a/docker/unified/nginx.conf b/docker/unified/nginx.conf
index 1e47b8619a..cbd5fee813 100644
--- a/docker/unified/nginx.conf
+++ b/docker/unified/nginx.conf
@@ -106,8 +106,8 @@ http {
add_header Cache-Control "public, immutable";
}
- # Cache static assets
- location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
+ # Cache static assets (but not API endpoints)
+ location ~* ^(?!/api/).*\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
diff --git a/frontend/index.html b/frontend/index.html
index 5dbaa18938..5042c6a41d 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -3,21 +3,21 @@
-
+
-
-
+
+
Stirling PDF
You need to enable JavaScript to run this app.
-
+