Compare commits

...
81 Commits
Author SHA1 Message Date
Reece Browne 12fff5b4a7 Merge branch 'main' into mac-print 2026-02-04 17:15:31 +00:00
ffd1abbdb3 Fix ClassCastException in extractBookmarks endpoint (#5578) (#5604)
## Description

Fixes #5578

This PR fixes a `ClassCastException` that occurs when calling
`/api/v1/general/extract-bookmarks`. The method was returning
`List<Map<String, Object>>` directly, but Spring MVC was wrapping it in
a `ResponseEntity`, causing a type mismatch.

## Changes

- Changed return type from `List<Map<String, Object>>` to
`ResponseEntity<List<Map<String, Object>>>`
- Wrapped return values with `ResponseEntity.ok(...)` to match Spring
MVC pattern
- Removed `@ResponseBody` annotation as it is not needed with
`ResponseEntity`

## Verification

This fix follows the same pattern used in other similar endpoints:
- `VerifyPDFController.verifyPDF()` returns
`ResponseEntity<List<PDFVerificationResult>>`
- `ValidateSignatureController.validateSignature()` returns
`ResponseEntity<List<SignatureValidationResult>>`

## Testing

The endpoint should now return a proper JSON response with the list of
bookmarks instead of throwing a 500 error.

---------

Co-authored-by: GitTensor Miner <miner@gittensor.io>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2026-02-03 09:48:09 +00:00
mrober01andAnthony Stirling 79bc62a5d2 update to add optional Zero Padding to page numbers (Bates Stamping).… (#5612)
… Useful in legal and other professional fields to have page numbers
written with padded 0s of a fixed width. This is also known as bates
stamping.

# Description of Changes

<!--
Another category is added to the add page number tool where it allows
for defining a 0 padded format. If left as 0, it will not added padded
0s and will be the current implementation.
-->

---

## Checklist

### General

- [x] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [x] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [x] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [x] I have performed a self-review of my own code
- [x] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [x] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2026-02-02 10:42:33 +00:00
Ludy 1c43bd363a Pin GitHub Actions and add runner hardening (#5628)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2026-01-31 21:00:07 +00:00
Anthony Stirling 4f404a1ccf Support multiple pipeline watch directories and configurable pipeline base path (#5545)
### Motivation
- Allow operators to configure a pipeline base directory and multiple
watched folders so the pipeline can monitor several directories and
subdirectories concurrently.
- Ensure scanning traverses subdirectories while skipping internal
processing folders (e.g. `processing`) and preserve existing behavior
for finished/output paths.
- Expose the new options in the server `settings.yml.template` and the
admin UI so paths can be edited from the web console.

### Description
- Added new `pipelineDir` and `watchedFoldersDirs` fields to
`ApplicationProperties.CustomPaths.Pipeline` and kept backward
compatibility with `watchedFoldersDir`
(app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java).
- Resolved pipeline base and multiple watched folder paths in
`RuntimePathConfig` and exposed `getPipelineWatchedFoldersPaths()`
(app/common/src/main/java/stirling/software/common/configuration/RuntimePathConfig.java).
- Updated `FileMonitor` to accept and register multiple root paths
instead of a single root
(app/common/src/main/java/stirling/software/common/util/FileMonitor.java).
- Updated `PipelineDirectoryProcessor` to iterate all configured watched
roots and to walk subdirectories while ignoring `processing` dirs
(app/core/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineDirectoryProcessor.java).
- Exposed the new settings in `settings.yml.template` and the admin UI,
including a multi-line `Textarea` to edit `watchedFoldersDirs`
(app/core/src/main/resources/settings.yml.template,
frontend/src/proprietary/components/shared/config/configSections/AdminGeneralSection.tsx).
- Adjusted unit test setup to account for list-based watched folders
(app/common/src/test/java/stirling/software/common/util/FileMonitorTest.java).

### Testing
- Ran formatting and build checks with `./gradlew spotlessApply` and
`./gradlew build` using Java 21 via
`JAVA_HOME=/root/.local/share/mise/installs/java/21.0.2
PATH=/root/.local/share/mise/installs/java/21.0.2/bin:$PATH ./gradlew
...`, but both runs failed due to Gradle plugin resolution being blocked
in this environment (plugin portal/network 403), so full
compilation/formatting could not complete.
- Confirmed the code compiles locally was not possible here; unit test
`FileMonitorTest` was updated to use the new API but was not executed
due to the blocked build.
- Changes were committed (`Support multiple pipeline watch directories`)
and the repository diff contains the listed file modifications.

------
[Codex
Task](https://chatgpt.com/codex/tasks/task_b_69741ecd17c883288d8085a63ccd66f4)
2026-01-31 20:59:25 +00:00
Anthony StirlingandJames Brunton 2ae413c5ea Stop attempting to refresh Spring tokens in desktop (#5610)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Co-authored-by: James Brunton <james@stirlingpdf.com>
2026-01-31 20:28:59 +00:00
Ludy 36358fc139 Update Python dependencies in requirements files (#5627)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2026-01-31 20:26:21 +00:00
Reece Browne d39a7ddda7 Bug/pageeditor virtualisation (#5614) 2026-01-31 20:07:45 +00:00
Ludy 789eaa263f feat(settings): display frontend/backend versions and warn on client-server mismatch (#5571)
# Description of Changes

## Summary
This PR improves the **Preferences → General → Software Updates**
section by:
- Showing **separate version labels** for **Frontend (Tauri client)**
and **Backend (server/AppConfig)** across all locales.
- Adding a **version mismatch detection** in `GeneralSection`, comparing
the Tauri app version against the backend `AppConfig` version and
displaying a **warning banner** when they differ.

## Why
Running a Tauri desktop client against a different backend version can
lead to:
- Compatibility issues (API/UI expectations drifting)
- Runtime errors due to schema/behavior changes
- Increased security risk if the client and server are not kept in sync

Surfacing both versions and warning on mismatch makes these situations
visible and easier to diagnose.


[stirling-pdf-2.4.1.exe.zip](https://github.com/user-attachments/files/24846696/stirling-pdf-2.4.1.exe.zip)

<img width="967" height="362" alt="image"
src="https://github.com/user-attachments/assets/8cd2a7d9-47ca-4caf-930b-4ec0a4c6317a"
/>


[stirling-pdf-2.4.0.exe.zip](https://github.com/user-attachments/files/24846864/stirling-pdf-2.4.0.exe.zip)

<img width="951" height="395" alt="image"
src="https://github.com/user-attachments/assets/70ba15eb-ec13-4737-9cae-1f6da3c18c1a"
/>

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2026-01-31 20:03:21 +00:00
Anthony Stirling 4575d7178b always allow tauri cors (#5616)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2026-01-31 19:52:48 +00:00
Ludy 3cdf363eab fix(auth): align token refresh handling with updated backend response (#5609)
# Description of Changes

This pull request updates the authentication token refresh response
structure to include both user and session information, and makes
corresponding adjustments in the backend, frontend, and tests to support
this change. Additionally, it adds improved logging to the frontend for
better debugging.

**Backend API response changes:**

* The `/api/v1/auth/refresh` endpoint now returns a response containing
both a `user` object and a nested `session` object with the new access
token and expiry, instead of returning the token fields at the top
level.

**Test updates:**

* The `refreshReturnsNewTokenWhenValid` test has been updated to expect
the new response structure, checking for `session.access_token` and
`session.expires_in` instead of the previous top-level fields.

**Frontend improvements:**

* Added a debug log message in `springAuthClient.ts` to indicate when
the token has been refreshed successfully, aiding in debugging and
monitoring.

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2026-01-31 19:42:41 +00:00
Ludy d990d4181d fix(frontend): prevent hydration errors in admin security form and improve autofill support (#5613)
# Description of Changes

## What was changed
- Updated several Mantine `label` compositions in
`AdminSecuritySection.tsx` to avoid invalid HTML nesting that can
trigger React hydration errors (e.g., a `<div>` rendered inside a
`<p>`).
- Changed `Group` used inside `NumberInput` / `Select` labels to render
as an inline element via `component="span"`.
- Added `name` attributes to multiple form controls (`Switch`, `Select`,
`NumberInput`, `Textarea`) to satisfy browser/autofill recommendations
and improve form field identification.

## Why the change was made
- Fixes the runtime warning/error:
- `In HTML, <div> cannot be a descendant of <p>. This will cause a
hydration error.`
- Caused by block-level wrappers inside Mantine `Text`/`p` label
rendering.
- Addresses the browser audit warning:
  - `A form field element should have an id or name attribute`
- Adding stable `name` attributes improves autofill behavior and form
accessibility tooling.

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2026-01-31 16:26:15 +00:00
numanair a46dc141d0 Update links to Docs (#5611)
# Description of Changes

Simply updated links to Docs. "Advanced Configuration" to
"Configuration".

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [x] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [x] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2026-01-31 12:43:31 +00:00
Dario Ghunney Ware 6fee27739c Fixed missing AuthnRequest bug (#5606) 2026-01-30 16:27:31 +00:00
James Brunton 1cc562a6b1 Stop type checking TypeScript files that won't be run (#5607)
# Description of Changes
This PR fixes false-positive TypeScript errors in our layered build
setup (core → proprietary → desktop) by ensuring each build’s typecheck
only evaluates files that are actually part of that build’s reachable
module graph. This prevents overridden core implementations from being
typechecked in higher-layer builds where they are effectively
unreachable due to alias-based overrides.

## Background

We maintain multiple build targets from a layered source tree:

- core: open source baseline
- proprietary: core + proprietary additions/overrides
- desktop: proprietary + desktop-specific additions/overrides

We implement overrides via paths/aliases such that placing a file in a
higher layer at the same relative path supersedes the lower-layer file
at runtime.

For safety, we run TypeScript typechecking independently per build
target to ensure all builds remain valid.

## Problem

Our existing tsconfig setup often typechecked files that are not
actually reachable in a given build. Specifically:

- When a file in core is overridden by a file in proprietary or desktop,
the overridden core file can still be included in the TypeScript Program
for the higher-layer build (typically due to broad include globs).
- This produces false-positive type errors in higher-layer typecheck
runs, even though those core files are effectively unreachable in the
build.

This created friction and noise, and meant we had to make unnecessary
changes to `core` to make the other builds happy, reducing type safety
in the process.

## Solution

This PR adjusts the tsconfig strategy so each build target's typecheck
is driven by reachable entrypoints rather than blanket inclusion of all
layer source trees. Concretely:

- Each build’s tsconfig now includes only:
- that build’s entrypoints and layer sources that are intended to be
compiled for the target
  - any shared/top-level sources required by the target
- Lower layers (e.g., core) are not globally included in higher-layer
builds; they are instead pulled in through module resolution only when
actually referenced (with paths ordering ensuring the correct override
wins).
- This means that we still check all the files that will actually be run
with whatever the overridden logic is, but avoid wasting time and
introducing false-positives by not checking files which have been
overridden.

## Notes
Unfortunately, the config we use for the type checking can't be the same
as the one we use for Vite in this strategy. Vite needs to know about
the entire source tree, so it can't only include the subfolders because
it causes build errors. Because of this, I've duplicated the existing
(valid) tsconfig files and use them for Vite. This is a little clunky
but it does the job. Some day hopefully I'll come back to it and be able
to figure out a nicer way to do it, but for now at least, this solves
the type checking issues without impacting the runtime builds.

Also, I noticed that `@desktop` is defined as an alias, which was
presumably missed when I was removing the self-aliases from the files. I
don't see why you'd ever need to have a desktop file reference
`@desktop` to say "import this but make it impossible for something else
to override the import". I've removed the `@desktop` alias in this PR
while I was in there.
2026-01-30 15:27:35 +00:00
stirlingbot[bot]andAnthony Stirling d90e51233d 🤖 format everything with pre-commit by stirlingbot (#5538)
Auto-generated by [create-pull-request][1] with **stirlingbot**

[1]: https://github.com/peter-evans/create-pull-request

---------

Signed-off-by: stirlingbot[bot] <stirlingbot[bot]@users.noreply.github.com>
Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2026-01-29 23:07:59 +00:00
1cd3e2846e feat(stamp): add dynamic variables and templates for stamp text customization (#5546)
# Description of Changes

This pull request improves the PDF stamping feature, particularly the
text stamping functionality, by introducing dynamic variables, improving
formatting flexibility, and refining positioning logic. The changes
include backend support for dynamic stamp variables (such as date, time,
filename, and metadata), improvements to text layout and positioning,
updates to the API and frontend for usability, and localization
enhancements for user guidance.

**Dynamic Stamp Variables and Text Processing:**

* Added support for dynamic variables in stamp text (e.g., `@date`,
`@time`, `@page_number`, `@filename`, `@uuid`, and metadata fields),
including custom date formats and escaping for literal `@` symbols. This
is handled by the new `processStampText` method in
`StampController.java`.
* Implemented validation and formatting for custom date variables,
ensuring only safe formats are accepted and providing user-friendly
error messages for invalid formats.

**Text Layout and Positioning Improvements:**

* Refactored text and image stamp positioning: now calculates line
heights, block heights, and widths for multi-line stamps, and adjusts
placement logic for more accurate alignment (top, center, bottom) and
margins.
* Updated the default font size for stamps to 40pt and improved font
size handling in both backend and frontend, including validation for
positive values

**API and Method Signature Updates:**

* Extended method signatures to include additional context (such as page
index and filename) for more powerful variable substitution in stamps
**Frontend and Localization Enhancements:**

* Added comprehensive help text, variable descriptions, and template
examples to the UI, making it easier for users to understand and use
dynamic stamp variables.
* Improved accessibility and clarity in the stamp formatting UI by
disabling controls appropriately and providing clearer descriptions.



<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>
Co-authored-by: ConnorYoh <40631091+ConnorYoh@users.noreply.github.com>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2026-01-29 21:16:14 +00:00
Reece Browne 03da0e7d68 Merge branch 'main' into mac-print 2026-01-29 20:24:16 +00:00
Reece 685b562d3d Attempt to fix print on mac 2026-01-29 18:05:01 +00:00
BitToby 080faf9353 fix: PDF Text Editor file open (#5572)
# Description of Changes

## Content
This pull requests fix the problem when opening the file in the ALPHA
feature PDF Text Editor.

## Page Where the Problem Occurred
http://localhost/pdf-text-editor

## Problem
convert_cff_to_ttf.py does not support named CLI arguments.

But Java is calling it like this:
convert_cff_to_ttf.py --input file.cff --output file.otf --to-unicode
file.tounicode

## Solved
convert_cff_to_ttf.py support named CLI arguments.

Closes #5518
---

## Checklist

### General

- [x] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [x] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [x] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [x] I have performed a self-review of my own code
- [x] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [x] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2026-01-29 16:52:21 +00:00
Dario Ghunney Ware d486bb4939 Fix Audit & Usage Analytics Sections (#5586)
Fixed issue where @lob annotation on audit column was casing
`org.postgresql.util.PSQLException: Large Objects may not be used in
auto-commit mode.` data retrieval issues with Postgres
2026-01-29 16:51:52 +00:00
Anthony Stirling 41f9929fd2 Fix tool disabling (#5585)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2026-01-29 14:36:10 +00:00
stirlingbot[bot] 0b0db1793e 🌐 Sync Translations + Update README Progress Table (#5581)
### Description of Changes

This Pull Request was automatically generated to synchronize updates to
translation files and documentation. Below are the details of the
changes made:

#### **1. Synchronization of Translation Files**
- Updated translation files
(`frontend/public/locales/*/translation.toml`) to reflect changes in the
reference file `en-GB/translation.toml`.
- Ensured consistency and synchronization across all supported language
files.
- Highlighted any missing or incomplete translations.
- **Format**: TOML

#### **2. Update README.md**
- Generated the translation progress table in `README.md` using
`counter_translation_v3.py`.
- Added a summary of the current translation status for all supported
languages.
- Included up-to-date statistics on translation coverage.

#### **Why these changes are necessary**
- Keeps translation files aligned with the latest reference updates.
- Ensures the documentation reflects the current translation progress.

---

Auto-generated by [create-pull-request][1].

[1]: https://github.com/peter-evans/create-pull-request

Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com>
2026-01-28 23:58:44 +00:00
Anthony Stirling f3cf747cfe possible login fixes (#5444)
# Description of Changes

Disable TLS checks and various cert checks to allow all sorts of
selfhost machines to be connected via tauri app

Version bump

Crop tool correctly shows ghostscript as optional so its not disabled on
java only installations

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2026-01-28 23:57:43 +00:00
dependabot[bot]andLudy e3982ed4c5 build(deps): bump pypdf from 6.6.0 to 6.6.2 in /testing/cucumber in the pip group across 1 directory (#5577)
Bumps the pip group with 1 update in the /testing/cucumber directory:
[pypdf](https://github.com/py-pdf/pypdf).

Updates `pypdf` from 6.6.0 to 6.6.2
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/py-pdf/pypdf/releases">pypdf's
releases</a>.</em></p>
<blockquote>
<h2>Version 6.6.2, 2026-01-26</h2>
<h2>What's new</h2>
<h3>Security (SEC)</h3>
<ul>
<li>Detect cyclic references when retrieving outlines (<a
href="https://redirect.github.com/py-pdf/pypdf/issues/3610">#3610</a>)
by <a
href="https://github.com/stefan6419846"><code>@​stefan6419846</code></a></li>
</ul>
<p><a href="https://github.com/py-pdf/pypdf/compare/6.6.1...6.6.2">Full
Changelog</a></p>
<h2>Version 6.6.1, 2026-01-25</h2>
<h2>What's new</h2>
<h3>Robustness (ROB)</h3>
<ul>
<li><code>/AcroForm</code> might be NullObject (<a
href="https://redirect.github.com/py-pdf/pypdf/issues/3601">#3601</a>)
by <a
href="https://github.com/joshkersey"><code>@​joshkersey</code></a></li>
<li>Handle missing font bounding boxes gracefully (<a
href="https://redirect.github.com/py-pdf/pypdf/issues/3600">#3600</a>)
by <a href="https://github.com/LudovA"><code>@​LudovA</code></a></li>
</ul>
<p><a href="https://github.com/py-pdf/pypdf/compare/6.6.0...6.6.1">Full
Changelog</a></p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/py-pdf/pypdf/blob/main/CHANGELOG.md">pypdf's
changelog</a>.</em></p>
<blockquote>
<h2>Version 6.6.2, 2026-01-26</h2>
<h3>Security (SEC)</h3>
<ul>
<li>Detect cyclic references when retrieving outlines (<a
href="https://redirect.github.com/py-pdf/pypdf/issues/3610">#3610</a>)</li>
</ul>
<p><a href="https://github.com/py-pdf/pypdf/compare/6.6.1...6.6.2">Full
Changelog</a></p>
<h2>Version 6.6.1, 2026-01-25</h2>
<h3>Robustness (ROB)</h3>
<ul>
<li><code>/AcroForm</code> might be NullObject (<a
href="https://redirect.github.com/py-pdf/pypdf/issues/3601">#3601</a>)</li>
<li>Handle missing font bounding boxes gracefully (<a
href="https://redirect.github.com/py-pdf/pypdf/issues/3600">#3600</a>)</li>
</ul>
<p><a href="https://github.com/py-pdf/pypdf/compare/6.6.0...6.6.1">Full
Changelog</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/py-pdf/pypdf/commit/ad47d50fd7475650d71913c8c0927e9a79249c75"><code>ad47d50</code></a>
REL: 6.6.2</li>
<li><a
href="https://github.com/py-pdf/pypdf/commit/b1282f8dcdc1a7b41ceab6740ffddfdf31b1fec1"><code>b1282f8</code></a>
SEC: Detect cyclic references when retrieving outlines (<a
href="https://redirect.github.com/py-pdf/pypdf/issues/3610">#3610</a>)</li>
<li><a
href="https://github.com/py-pdf/pypdf/commit/f18e13c91cd47fa740997df4a7307fbc976388e1"><code>f18e13c</code></a>
REL: 6.6.1</li>
<li><a
href="https://github.com/py-pdf/pypdf/commit/19735763b856cccf0f69630d0f582a448ec5d8bb"><code>1973576</code></a>
DEV: Bump wheel from 0.44.0 to 0.46.2 (<a
href="https://redirect.github.com/py-pdf/pypdf/issues/3608">#3608</a>)</li>
<li><a
href="https://github.com/py-pdf/pypdf/commit/4740225eaa67ad2e032e63d0453ea6c80bcae158"><code>4740225</code></a>
ROB: <code>/AcroForm</code> might be NullObject (<a
href="https://redirect.github.com/py-pdf/pypdf/issues/3601">#3601</a>)</li>
<li><a
href="https://github.com/py-pdf/pypdf/commit/26fd6388754ed167e862bdd7a3eba614da191d34"><code>26fd638</code></a>
ROB: Handle missing font bounding boxes gracefully (<a
href="https://redirect.github.com/py-pdf/pypdf/issues/3600">#3600</a>)</li>
<li><a
href="https://github.com/py-pdf/pypdf/commit/affe8eddf185b2875dcfa45090b7d09f06d3ba12"><code>affe8ed</code></a>
DEV: Bump virtualenv from 20.27.0 to 20.36.1 (<a
href="https://redirect.github.com/py-pdf/pypdf/issues/3597">#3597</a>)</li>
<li><a
href="https://github.com/py-pdf/pypdf/commit/df1b91da41e38ffaf0815e3fd687c037b375c728"><code>df1b91d</code></a>
DEV: Add missing dependency to URL check</li>
<li>See full diff in <a
href="https://github.com/py-pdf/pypdf/compare/6.6.0...6.6.2">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=pypdf&package-manager=pip&previous-version=6.6.0&new-version=6.6.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/Stirling-Tools/Stirling-PDF/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ludy <Ludy87@users.noreply.github.com>
2026-01-28 16:11:15 +00:00
Balázs SzücsandAnthony Stirling 5c7d675960 deps(embedPDF): Bump codebase to embedPDF v2.3.0 and adjust codebase for new features (#5567)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2026-01-28 16:10:43 +00:00
Anthony Stirling 7fc6ec5fe1 tool tags (#5568)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2026-01-28 10:36:56 +00:00
Anthony Stirling 7722001463 xframe fix new (#5580)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2026-01-28 10:36:21 +00:00
LudyandCopilot 43d4b46b31 deps(frontend, tauri): update Tauri, Rust crates, and frontend dependencies (#5569)
# Description of Changes

This pull request primarily updates dependencies for both the frontend
JavaScript and Rust (Tauri) codebases, and refactors the
`MobileUploadModal` component to consistently use a centralized API
client for backend communication. The refactor improves code
consistency, error handling, and logging in the file upload workflow.

**Dependency updates**

* Updated several Tauri-related dependencies in both
`frontend/package.json` and `frontend/src-tauri/Cargo.toml` to their
latest versions, including `@tauri-apps/api`, `@tauri-apps/plugin-fs`,
`@tauri-apps/plugin-http`, `@tauri-apps/plugin-shell`, and associated
Rust crates. This ensures better compatibility, security, and access to
new features.
[[1]](diffhunk://#diff-da6498268e99511d9ba0df3c13e439d10556a812881c9d03955b2ef7c6c1c655L46-R49)
[[2]](diffhunk://#diff-da6498268e99511d9ba0df3c13e439d10556a812881c9d03955b2ef7c6c1c655L129-R132)
[[3]](diffhunk://#diff-91e702206f8c6459b43ae72dbd6abfed8104de661dd239d13956985210f67fd0L21-R35)
* Updated `@iconify-json/material-symbols` and `@tauri-apps/cli` in
`package.json` for improved icon support and build tooling.

**Refactor: API client usage in `MobileUploadModal`**

* Replaced all direct `fetch` calls in `MobileUploadModal.tsx` with the
centralized `apiClient`, standardizing backend requests and improving
maintainability.
[[1]](diffhunk://#diff-fafb4b340343062aba7b763dea5e6e13e0e330ab2ac7dfd04a2032ba79620c8aR13)
[[2]](diffhunk://#diff-fafb4b340343062aba7b763dea5e6e13e0e330ab2ac7dfd04a2032ba79620c8aL84-R98)
[[3]](diffhunk://#diff-fafb4b340343062aba7b763dea5e6e13e0e330ab2ac7dfd04a2032ba79620c8aL116-R122)
[[4]](diffhunk://#diff-fafb4b340343062aba7b763dea5e6e13e0e330ab2ac7dfd04a2032ba79620c8aL130-R138)
[[5]](diffhunk://#diff-fafb4b340343062aba7b763dea5e6e13e0e330ab2ac7dfd04a2032ba79620c8aL160-R177)
[[6]](diffhunk://#diff-fafb4b340343062aba7b763dea5e6e13e0e330ab2ac7dfd04a2032ba79620c8aL187-R207)
* Improved error handling, status checks, and logging throughout the
upload and session management flow, making debugging easier and the user
experience more robust.
[[1]](diffhunk://#diff-fafb4b340343062aba7b763dea5e6e13e0e330ab2ac7dfd04a2032ba79620c8aL84-R98)
[[2]](diffhunk://#diff-fafb4b340343062aba7b763dea5e6e13e0e330ab2ac7dfd04a2032ba79620c8aL130-R138)
[[3]](diffhunk://#diff-fafb4b340343062aba7b763dea5e6e13e0e330ab2ac7dfd04a2032ba79620c8aL148-R153)
[[4]](diffhunk://#diff-fafb4b340343062aba7b763dea5e6e13e0e330ab2ac7dfd04a2032ba79620c8aL160-R177)
[[5]](diffhunk://#diff-fafb4b340343062aba7b763dea5e6e13e0e330ab2ac7dfd04a2032ba79620c8aL187-R207)

**Session cleanup improvements**

* Ensured that mobile scanner sessions are reliably cleaned up both when
the modal closes and when the component unmounts, using the `apiClient`
and React's effect cleanup mechanism.

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-28 10:35:59 +00:00
Anthony Stirling eee17d4d19 pipeline fixes for naming issues (#5570)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2026-01-28 10:35:46 +00:00
LudyandCopilot c40144db19 chore(ci): remove automated version sync job from build workflow (#5547)
# Description of Changes

This pull request removes the `sync-versions` job from the GitHub
Actions workflow configuration. The `sync-versions` job was responsible
for ensuring that version numbers in certain frontend files matched the
version defined in `build.gradle`, and for posting a comment on pull
requests if discrepancies were found.

Workflow cleanup:

* Removed the entire `sync-versions` job from
`.github/workflows/build.yml`, including steps for syncing app versions,
checking for changes, and posting PR comments when version mismatches
were detected.

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-24 23:45:48 +00:00
Anthony Stirling c77cd73deb translations (#5541)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2026-01-24 00:02:31 +00:00
Anthony Stirling b76f3662e4 Export folder scanning (#5544)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2026-01-23 22:34:39 +00:00
Balázs Szücs 188408fc1e feat(compliance): implement compliance verfication for get info on PDF (#5435)
# Description of Changes


This PR replaces the legacy, fragile PDF/A validation logic (which
relied on manual regex parsing and PDFBox Preflight) with the robust
**VeraPDF** integration. Additionally, it introduces a new check for
**SEC/EDGAR compliance** and refactors the frontend report view to
display detailed verification results.

### Key Changes

**Backend (`GetInfoOnPDF.java`):**

* **VeraPDF Integration:** Replaced the custom `checkForStandard` and
`validatePdfAWithPreflight` methods with `VeraPDFService.validatePDF()`.
This aligns validation with industry standards.
* **Code Cleanup:** Removed approximately ~200 lines of technical debt,
including raw XML regex parsing and file-locking prone `PreflightParser`
logic.
* **SEC Compliance:** Added `isSECCompliant()` logic to validate
documents against typical EDGAR requirements:
* No Encryption.
* No Active Content (JavaScript).
* No External Links.
* No Embedded Files.
* No AcroForms.



**Frontend (`GetPdfInfo`):**

* **New Component:** Created `ComplianceSection.tsx` to handle the
complexity of compliance reporting.
* **Enhanced UI:**
* Added visual badges (Passed/Failed/Not Detected) using
`CheckIcon`/`CloseIcon`.
* Added support for displaying the detailed VeraPDF summary alongside
legacy boolean flags.
* Improved handling of "Not PDF/A" states.


### Motivation

The previous validation logic was prone to false positives/negatives and
memory issues with large files (due to Preflight). Moving to VeraPDF
provides accurate, profile-based validation (e.g., PDF/A-1b, 2b, etc.).
The SEC check satisfies a growing need for users validating documents
for financial filings.



<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [X] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [X] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [X] I have performed a self-review of my own code
- [X] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [X] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [X] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>
Signed-off-by: brios <brios@heim-041-30.jkh.uni-linz.ac.at>
2026-01-23 21:36:35 +00:00
LudyandCopilot 0b86dd79d3 feat(security): add TOTP-based multi-factor authentication with backend and UI support (#5417)
# Description of Changes

This pull request introduces several improvements and new features
across the authentication and admin data APIs, with a particular focus
on multi-factor authentication (MFA) support and better handling of user
settings. The changes include integrating MFA status into account data
responses, masking sensitive user settings in admin views, and
refactoring code to use more robust user creation methods. Additionally,
there are minor code cleanups and consistency improvements.

### Multi-factor Authentication (MFA) Integration

* Added `mfaEnabled` and `mfaRequired` fields to the `AccountData`
response, populated using the new `MfaService`, to provide clients with
MFA status information for users.
[[1]](diffhunk://#diff-2ead183708656f2c6894b28457623820c83b1ed4b0814533caa0e8f0dd6fbcd1R430-R431)
[[2]](diffhunk://#diff-2ead183708656f2c6894b28457623820c83b1ed4b0814533caa0e8f0dd6fbcd1R72)
[[3]](diffhunk://#diff-2ead183708656f2c6894b28457623820c83b1ed4b0814533caa0e8f0dd6fbcd1L83-R86)
[[4]](diffhunk://#diff-2ead183708656f2c6894b28457623820c83b1ed4b0814533caa0e8f0dd6fbcd1R98)
[[5]](diffhunk://#diff-2ead183708656f2c6894b28457623820c83b1ed4b0814533caa0e8f0dd6fbcd1R574-R575)

### User Settings Handling and Security

* Admin settings API now returns user settings for each user, with the
`mfaSecret` field masked to protect sensitive information. This is
achieved by fetching settings via `findByIdWithSettings` and
copying/masking the relevant field before returning.
[[1]](diffhunk://#diff-2ead183708656f2c6894b28457623820c83b1ed4b0814533caa0e8f0dd6fbcd1R252-R259)
[[2]](diffhunk://#diff-2ead183708656f2c6894b28457623820c83b1ed4b0814533caa0e8f0dd6fbcd1L302-R322)
[[3]](diffhunk://#diff-2ead183708656f2c6894b28457623820c83b1ed4b0814533caa0e8f0dd6fbcd1R378)
[[4]](diffhunk://#diff-2ead183708656f2c6894b28457623820c83b1ed4b0814533caa0e8f0dd6fbcd1R563)

### Refactoring and Code Consistency

* Refactored user creation in `InitialSecuritySetup` to use the
`SaveUserRequest` builder and `saveUserCore` for better maintainability
and clarity, replacing direct calls to `saveUser`.
[[1]](diffhunk://#diff-0c7960a6283a07c4905ac9785b2820b412574c9f86918ada30caba0356d34850R22)
[[2]](diffhunk://#diff-0c7960a6283a07c4905ac9785b2820b412574c9f86918ada30caba0356d34850L116-R124)
[[3]](diffhunk://#diff-0c7960a6283a07c4905ac9785b2820b412574c9f86918ada30caba0356d34850L130-R144)
[[4]](diffhunk://#diff-0c7960a6283a07c4905ac9785b2820b412574c9f86918ada30caba0356d34850L140-R160)
* Standardized checks for internal team membership by comparing with
`TeamService.INTERNAL_TEAM_NAME` on the left side for consistency.
[[1]](diffhunk://#diff-2ead183708656f2c6894b28457623820c83b1ed4b0814533caa0e8f0dd6fbcd1L267-R273)
[[2]](diffhunk://#diff-2ead183708656f2c6894b28457623820c83b1ed4b0814533caa0e8f0dd6fbcd1L332-R351)
[[3]](diffhunk://#diff-2ead183708656f2c6894b28457623820c83b1ed4b0814533caa0e8f0dd6fbcd1L421-R443)
[[4]](diffhunk://#diff-2ead183708656f2c6894b28457623820c83b1ed4b0814533caa0e8f0dd6fbcd1L449-R471)
[[5]](diffhunk://#diff-2ead183708656f2c6894b28457623820c83b1ed4b0814533caa0e8f0dd6fbcd1L462-R485)

### API and DTO Changes

* Changed the login API to accept `UsernameAndPassMfa` instead of
`UsernameAndPass`, paving the way for MFA code support in authentication
requests.
[[1]](diffhunk://#diff-9ca4f9246abe79368552264e2e18d7ed039e084c70c0794eb02cfd1b75fbd8a8L30-R41)
[[2]](diffhunk://#diff-9ca4f9246abe79368552264e2e18d7ed039e084c70c0794eb02cfd1b75fbd8a8L61-R71)
* Updated import statements and controller dependencies to include new
DTOs and services related to MFA.
[[1]](diffhunk://#diff-9ca4f9246abe79368552264e2e18d7ed039e084c70c0794eb02cfd1b75fbd8a8L14-R19)
[[2]](diffhunk://#diff-9ca4f9246abe79368552264e2e18d7ed039e084c70c0794eb02cfd1b75fbd8a8R56-R57)

These updates improve security, prepare the system for MFA rollout, and
make admin and authentication APIs more robust and informative.

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-23 21:34:57 +00:00
Anthony Stirling 0436460c03 V2 Show enterprise demo messages for audit and usage (#5226)
## Summary
- show demo data for audit and usage sections when enterprise licensing
is unavailable
- add enterprise-required banner messaging and reuse demo content
instead of erroring
- update translations for the new enterprise notice

## Testing
- Not run (not requested)


------
[Codex
Task](https://chatgpt.com/codex/tasks/task_b_693af73ad9248328885eb8bb81ccf51a)
2026-01-23 16:26:46 +00:00
Balázs SzücsandAnthony Stirling 4d84dcdd42 [V2] feat(convert): add PDF/X export option (#5285)
# Description of Changes

This pull request adds support for converting PDF files to PDF/X format
in the frontend, alongside the existing PDF/A conversion. The changes
include UI updates, translation strings, configuration, and processing
logic to enable PDF to PDF/X conversion using the same backend endpoint
as PDF/A, with appropriate user options and warnings.

**PDF/X Conversion Support:**

- Added a new `ConvertToPdfxSettings` component for configuring PDF/X
conversion options, including output format selection and digital
signature warnings.
(`frontend/src/core/components/tools/convert/ConvertToPdfxSettings.tsx`)
- Updated the conversion settings UI to display PDF/X options when
converting from PDF to PDF/X.
(`frontend/src/core/components/tools/convert/ConvertSettings.tsx`)

**Configuration and Processing Logic:**

- Extended conversion constants and parameters to recognize PDF/X as a
target format, map it to the PDF/A backend endpoint, and ensure correct
file naming and processing behavior.
(`frontend/src/core/constants/convertConstants.ts`,
`frontend/src/core/hooks/tools/convert/useConvertOperation.ts`,
`frontend/src/core/hooks/tools/convert/useConvertParameters.ts`)
- Ensured that PDF/X conversions are processed separately per file,
similar to PDF/A.
(`frontend/src/core/hooks/tools/convert/useConvertOperation.ts`)

**UI and Translation Updates:**

- Updated translation strings to include PDF/X options, descriptions,
and warnings. (`frontend/public/locales/en-GB/translation.toml`)

<img width="366" height="998" alt="image"
src="https://github.com/user-attachments/assets/b28fa095-9350-4db2-a0b5-bddcf003fa46"
/>


<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2026-01-23 15:50:39 +00:00
stirlingbot[bot] 039e3b5fa8 Update Backend 3rd Party Licenses (#5539)
Auto-generated by stirlingbot[bot]

This PR updates the backend license report based on dependency changes.

Signed-off-by: stirlingbot[bot] <stirlingbot[bot]@users.noreply.github.com>
Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com>
2026-01-23 11:44:45 +00:00
Joey 15c5b0eb92 Date and time placeholders in addTextStamp are not working (#5446)
# Description of Changes

The processedStampText variable should be used as parameter when
splitting text into lines. This never happened so date and time
placeholder were never used.

---

## Checklist

### General

- [ x ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ x ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ x ] I have performed a self-review of my own code
- [ x ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2026-01-23 11:43:27 +00:00
Anthony Stirling 36c9369404 removal of old java desktop + workspace change in tauri (#5459)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2026-01-23 11:42:33 +00:00
Anthony Stirling 43f3261972 Fix V2 SSO user creation (#5079)
# Description of Changes


---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2026-01-22 21:24:49 +00:00
dependabot[bot] 3d7efc5d94 build(deps): bump debian from f668110 to ed542b2 in /docker/embedded (#5495)
Bumps debian from `f668110` to `ed542b2`.


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=debian&package-manager=docker&previous-version=stable-slim&new-version=stable-slim)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-22 20:12:44 +00:00
dependabot[bot] ebf256fd50 build(deps): bump node from f4769ca to e80397b in /docker/frontend (#5498)
[//]: # (dependabot-start)
⚠️  **Dependabot is rebasing this PR** ⚠️ 

Rebasing might not happen immediately, so don't worry if this takes some
time.

Note: if you make any changes to this PR yourself, they will take
precedence over the rebase.

---

[//]: # (dependabot-end)

Bumps node from `f4769ca` to `e80397b`.


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=node&package-manager=docker&previous-version=25-alpine&new-version=25-alpine)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-22 20:12:29 +00:00
dependabot[bot] f678e1d2e3 build(deps): bump nginx from c083c37 to b0f7830 in /docker/frontend (#5500)
Bumps nginx from `c083c37` to `b0f7830`.


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=nginx&package-manager=docker&previous-version=alpine&new-version=alpine)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-22 20:12:12 +00:00
Ludy 81c14351ee fix(common): 🛡️CWE-681 & CWE-197 eliminate tainted numeric casts in size parsing by using BigDecimal with range guards (#5521)
# Description of Changes

This pull request refactors and improves the logic for converting
human-readable size strings (like "10MB", "2.5GB") to bytes in the
`GeneralUtils` utility class. The main enhancement is switching from
imprecise floating-point arithmetic to `BigDecimal` for more accurate
and robust conversions, and centralizing the conversion logic to reduce
code duplication and improve maintainability.

**Improvements to size conversion logic:**

* Replaced all floating-point arithmetic in `convertSizeToBytes` with
`BigDecimal` operations to ensure precision and to handle large values
more safely.
* Introduced a new private method `toBytes(BigDecimal value, int
powerOf1024)` to centralize and standardize the conversion from size
units to bytes, including error handling for negative and excessively
large values.
* Added constants `KIB` and `LONG_MAX_DECIMAL` for improved readability
and maintainability of size calculations.
* Added a helper method `parseSizeValue(String value)` to consistently
parse size values as `BigDecimal`.
* Updated imports to include `BigDecimal` and `RoundingMode` for the new
conversion logic.

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2026-01-22 19:48:49 +00:00
Ludy 3711d8d6b1 refactor(security): clean up SSO handling, harden URL parsing, and bump dependencies (#5523)
# Description of Changes

This PR performs a broad cleanup and refactor across the security, SSO,
and dependency layers to improve correctness, maintainability, and
robustness.

### What was changed

- **SSO / Authentication cleanup**
- Removed deprecated and ambiguous `SSO` authentication handling in
favor of explicit `OAUTH2` and `SAML2`.
- Introduced a centralized helper (`isSsoAuthenticationTypeByUsername`)
to consistently detect SSO-backed users.
- Hardened user creation logic to strictly validate authentication types
and reject invalid values.
- Updated OAuth2 and SAML2 authentication success handlers to use
unified SSO detection logic and clearer control flow.
  - Adjusted tests to reflect the new canonical authentication types.

- **Security & robustness improvements**
- Replaced direct `new URL(...)` usage with `URI.create(...).toURL()` to
avoid malformed URL edge cases.
- Hardened `Referer` parsing logic to safely handle invalid or host-less
URIs.
- Improved string comparison patterns (`"literal".equals(x)`) to avoid
potential `NullPointerException`s.

- **Controller and API cleanup**
- Removed large blocks of unused and legacy admin settings endpoints
from `SettingsController`.
- Updated OpenAPI annotations to use `requiredMode` instead of
deprecated `required`.

- **Dependency and build maintenance**
  - Updated Spring Boot from `3.5.7` to `3.5.9`.
- Updated multiple dependencies (Spring Security, Jackson, Micrometer,
Jetty, Hibernate, SnakeYAML, Springdoc, Swagger UI, etc.).
- Synced dependency versions in `3rdPartyLicenses.json` and removed
duplicate or obsolete entries.
- Modernized Gradle DSL usage (`url =`, `username =`,
`allowInsecureProtocol = true`).
- Ensured Spotless disabling applies consistently across all
subprojects.
  - Added `.build-cache` to `.gitignore`.

### Why the change was made

- To eliminate legacy and ambiguous SSO handling that could lead to
incorrect authentication decisions.
- To improve security and stability when dealing with user-controlled
URLs and headers.
- To reduce technical debt by removing unused controllers and deprecated
patterns.
- To keep dependencies up to date and aligned with the current Spring
Boot release.
- To improve overall code clarity, consistency, and long-term
maintainability.

---

This pull request contains dependency updates, minor code cleanups, and
some refactoring to improve maintainability and correctness. The most
significant change is the removal of all admin settings endpoints
(GET/POST) from the `SettingsController`, which impacts how application
settings can be managed via the API. Additionally, there are dependency
version bumps, minor improvements to static resource checks, and small
refactors in certificate download logic and Telegram bot service.

**Major API changes:**

* Removed all admin settings endpoints (general, security, connections,
privacy, advanced) from `SettingsController`, including both GET and
POST handlers for updating and retrieving settings. This eliminates the
ability to manage these settings via the API.

**Dependency updates:**

* Upgraded `snakeyaml-engine` from 2.10 to 3.0.1 and
`springdoc-openapi-starter-webmvc-ui` from 2.8.14 to 2.8.15 in
`build.gradle`.

**Refactoring and bug fixes:**

* Refactored static resource check in `RequestUriUtils.isStaticResource`
to use constant-first string comparison for better null safety and
clarity.
* Updated certificate download logic in `CertificateValidationService`
to use `URI.create(urlStr).toURL()` instead of `new URL(urlStr)` for
improved URL parsing and error handling.
[[1]](diffhunk://#diff-d2646f37bfd3e0963cbce16ab13edb52f2092795f54203b999dd82651154f26dL513-R514)
[[2]](diffhunk://#diff-d2646f37bfd3e0963cbce16ab13edb52f2092795f54203b999dd82651154f26dL703-R704)
* Refactored `TelegramPipelineBot` to consistently use
`telegramProperties.getBotToken()` instead of `getBotToken()`, and
removed the `getBotToken()` method override.
[[1]](diffhunk://#diff-a2466b92f58750ea37960cd1533e3194d9ecc3b4ef5ad7b64a017ee0e636ad93L85-R85)
[[2]](diffhunk://#diff-a2466b92f58750ea37960cd1533e3194d9ecc3b4ef5ad7b64a017ee0e636ad93L395-R395)
[[3]](diffhunk://#diff-a2466b92f58750ea37960cd1533e3194d9ecc3b4ef5ad7b64a017ee0e636ad93L519-L523)

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2026-01-22 19:39:34 +00:00
LudyandCopilot a3b2a9b3e3 feat(frontend): enhance icon detection and update config navigation icon (#5524)
# Description of Changes

### What was changed
- Extended the `generate-icons.js` script to detect additional icon
usage patterns:
  - `<LocalIcon icon='...' />` with single-quoted icon names
- Icon definitions inside configuration objects (`icon: '...'` or `icon:
"..."`)
- Updated the configuration navigation icon for the *Connection Mode*
section from `cloud-rounded` to `desktop-cloud-rounded` in both
hook-based and static section definitions.

### Why the change was made
- The icon generation script previously missed icons referenced via
`LocalIcon` with single quotes and icons defined in config objects,
which could lead to missing assets in builds.
- The navigation icon update aligns the UI with the intended
desktop-specific icon semantics.


---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-22 19:37:37 +00:00
dependabot[bot] dd7925fbf4 build(deps): bump io.micrometer:micrometer-core from 1.16.1 to 1.16.2 (#5466)
Bumps
[io.micrometer:micrometer-core](https://github.com/micrometer-metrics/micrometer)
from 1.16.1 to 1.16.2.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/micrometer-metrics/micrometer/releases">io.micrometer:micrometer-core's
releases</a>.</em></p>
<blockquote>
<h2>1.16.2</h2>
<h2>🐞 Bug Fixes</h2>
<ul>
<li>CaffeineCacheMetrics nullability is incompatible with Caffeine 3.2.3
<a
href="https://redirect.github.com/micrometer-metrics/micrometer/issues/6973">#6973</a></li>
<li>ExecutorServiceMetrics: repeatedly logs exception when monitoring
<code>ThreadPerTaskExecutor</code> without <code>--add-opens</code> <a
href="https://redirect.github.com/micrometer-metrics/micrometer/issues/6726">#6726</a></li>
</ul>
<h2>🔨 Dependency Upgrades</h2>
<ul>
<li>Bump ch.qos.logback:logback-classic from 1.5.21 to 1.5.24 <a
href="https://redirect.github.com/micrometer-metrics/micrometer/pull/7027">#7027</a></li>
<li>Bump com.netflix.spectator:spectator-reg-atlas from 1.9.2 to 1.9.3
<a
href="https://redirect.github.com/micrometer-metrics/micrometer/pull/7051">#7051</a></li>
<li>Bump com.uber.nullaway:nullaway from 0.12.14 to 0.12.15 <a
href="https://redirect.github.com/micrometer-metrics/micrometer/pull/7020">#7020</a></li>
<li>Bump grpc from 1.76.1 to 1.76.2 <a
href="https://redirect.github.com/micrometer-metrics/micrometer/pull/6952">#6952</a></li>
<li>Bump maven-resolver from 1.9.24 to 1.9.25 <a
href="https://redirect.github.com/micrometer-metrics/micrometer/pull/6965">#6965</a></li>
<li>Bump org.apache.httpcomponents.client5:httpclient5 from 5.5.1 to
5.5.2 <a
href="https://redirect.github.com/micrometer-metrics/micrometer/pull/7019">#7019</a></li>
<li>Bump org.junit:junit-bom from 5.14.1 to 5.14.2 <a
href="https://redirect.github.com/micrometer-metrics/micrometer/pull/7056">#7056</a></li>
<li>Bump spring6 from 6.2.14 to 6.2.15 <a
href="https://redirect.github.com/micrometer-metrics/micrometer/pull/6971">#6971</a></li>
<li>Bump testcontainers from 1.21.3 to 1.21.4 <a
href="https://redirect.github.com/micrometer-metrics/micrometer/pull/6993">#6993</a></li>
</ul>
<h2>❤️ Contributors</h2>
<p>Thank you to all the contributors who worked on this release:</p>
<p><a
href="https://github.com/MariusVolkhart"><code>@​MariusVolkhart</code></a>
and <a href="https://github.com/izeye"><code>@​izeye</code></a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/micrometer-metrics/micrometer/commit/6286e854f943bce9a2daf1214ce3cef6c7aad84d"><code>6286e85</code></a>
Bump ch.qos.logback:logback-classic from 1.5.23 to 1.5.24 (<a
href="https://redirect.github.com/micrometer-metrics/micrometer/issues/7059">#7059</a>)</li>
<li><a
href="https://github.com/micrometer-metrics/micrometer/commit/5263cc740b73fd414ce30a910c78bc9bbb522854"><code>5263cc7</code></a>
Merge branch '1.15.x' into 1.16.x</li>
<li><a
href="https://github.com/micrometer-metrics/micrometer/commit/0523810d0d3aada310d9ed91e04d52aa420ffd9f"><code>0523810</code></a>
Polish CurrentObservationTest (<a
href="https://redirect.github.com/micrometer-metrics/micrometer/issues/7048">#7048</a>)</li>
<li><a
href="https://github.com/micrometer-metrics/micrometer/commit/3165fdffb9b1f7099f017805b3322c2a050653aa"><code>3165fdf</code></a>
Merge branch '1.15.x' into 1.16.x</li>
<li><a
href="https://github.com/micrometer-metrics/micrometer/commit/50ba16bb3d55912e7f25a3903aa2a3e57f3eb98a"><code>50ba16b</code></a>
Handle when ThreadPerTaskExecutor.threadCount() is not available (<a
href="https://redirect.github.com/micrometer-metrics/micrometer/issues/7045">#7045</a>)</li>
<li><a
href="https://github.com/micrometer-metrics/micrometer/commit/c38e88c7b2f0e6fd2e6e48ccc1949b389d61f2a4"><code>c38e88c</code></a>
Bump org.junit:junit-bom from 5.14.1 to 5.14.2 (<a
href="https://redirect.github.com/micrometer-metrics/micrometer/issues/7056">#7056</a>)</li>
<li><a
href="https://github.com/micrometer-metrics/micrometer/commit/22ba833062aacac4d877686e76c539c873d7c659"><code>22ba833</code></a>
Bump com.netflix.spectator:spectator-reg-atlas from 1.9.2 to 1.9.3 (<a
href="https://redirect.github.com/micrometer-metrics/micrometer/issues/7051">#7051</a>)</li>
<li><a
href="https://github.com/micrometer-metrics/micrometer/commit/916ced5cbc257a865c84dd2c59d6c2285c087e1b"><code>916ced5</code></a>
Merge branch '1.15.x' into 1.16.x</li>
<li><a
href="https://github.com/micrometer-metrics/micrometer/commit/50d273be26275a4e0d5a1549bb7ab6453e085bbe"><code>50d273b</code></a>
Merge branch '1.14.x' into 1.15.x</li>
<li><a
href="https://github.com/micrometer-metrics/micrometer/commit/6b44cf2b2148cd655c98592b183843823074f1d7"><code>6b44cf2</code></a>
Relax time limit for
PushMeterRegistryTest.waitForScheduledPublishToFinish_wh...</li>
<li>Additional commits viewable in <a
href="https://github.com/micrometer-metrics/micrometer/compare/v1.16.1...v1.16.2">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=io.micrometer:micrometer-core&package-manager=gradle&previous-version=1.16.1&new-version=1.16.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-22 19:27:23 +00:00
dependabot[bot] 1b1d17f6f5 build(deps): bump logback from 1.5.23 to 1.5.24 (#5468)
Bumps `logback` from 1.5.23 to 1.5.24.
Updates `ch.qos.logback:logback-core` from 1.5.23 to 1.5.24
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/qos-ch/logback/releases">ch.qos.logback:logback-core's
releases</a>.</em></p>
<blockquote>
<h2>Logback 1.5.24</h2>
<p><strong>2026-01-06 Release of logback version 1.5.24</strong></p>
<p>• Added ExpressionPropertyCondition a PropertyCondition that can
evaluate boolean expressions similar to Java. See <a
href="https://logback.qos.ch/manual/configuration.html#conditionalExp">the
relevant documentation</a> for further details.</p>
<p>• A bit-wise identical binary of this version can be reproduced by
building from source code at commit
62bc5fc245dd3a52f3dd45e232733f4cefb4806d associated with the tag
v_1.5.24. Release built using Java &quot;21&quot; 2023-10-17 LTS build
21.0.1.+12-LTS-29 under Linux Debian 11.6.</p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/qos-ch/logback/commit/62bc5fc245dd3a52f3dd45e232733f4cefb4806d"><code>62bc5fc</code></a>
prepare release 1.5.24</li>
<li><a
href="https://github.com/qos-ch/logback/commit/aac604d7e8ab4f91f240256755f3a09e53e909f3"><code>aac604d</code></a>
typo fix of local variable name</li>
<li><a
href="https://github.com/qos-ch/logback/commit/8a6df9e5c4e935d158b85811d33f72d10373d914"><code>8a6df9e</code></a>
ExpressionPropertyCondition constructor should be public</li>
<li><a
href="https://github.com/qos-ch/logback/commit/95e588c4e37b3e76ff2a5c13e60d7e0485d43fb2"><code>95e588c</code></a>
minor changes in ExpressionPropertyCondition</li>
<li><a
href="https://github.com/qos-ch/logback/commit/859f5a1f34cdec0f63a1830394df8238e780a9f4"><code>859f5a1</code></a>
added ExpressionPropertyCondition capable of parsing logical expressions
on p...</li>
<li><a
href="https://github.com/qos-ch/logback/commit/348075adfa7cdd8f7bba60225ec570efb7761d3c"><code>348075a</code></a>
start work on 1.5.24-SNAPSHOT</li>
<li>See full diff in <a
href="https://github.com/qos-ch/logback/compare/v_1.5.23...v_1.5.24">compare
view</a></li>
</ul>
</details>
<br />

Updates `ch.qos.logback:logback-classic` from 1.5.23 to 1.5.24
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/qos-ch/logback/releases">ch.qos.logback:logback-classic's
releases</a>.</em></p>
<blockquote>
<h2>Logback 1.5.24</h2>
<p><strong>2026-01-06 Release of logback version 1.5.24</strong></p>
<p>• Added ExpressionPropertyCondition a PropertyCondition that can
evaluate boolean expressions similar to Java. See <a
href="https://logback.qos.ch/manual/configuration.html#conditionalExp">the
relevant documentation</a> for further details.</p>
<p>• A bit-wise identical binary of this version can be reproduced by
building from source code at commit
62bc5fc245dd3a52f3dd45e232733f4cefb4806d associated with the tag
v_1.5.24. Release built using Java &quot;21&quot; 2023-10-17 LTS build
21.0.1.+12-LTS-29 under Linux Debian 11.6.</p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/qos-ch/logback/commit/62bc5fc245dd3a52f3dd45e232733f4cefb4806d"><code>62bc5fc</code></a>
prepare release 1.5.24</li>
<li><a
href="https://github.com/qos-ch/logback/commit/aac604d7e8ab4f91f240256755f3a09e53e909f3"><code>aac604d</code></a>
typo fix of local variable name</li>
<li><a
href="https://github.com/qos-ch/logback/commit/8a6df9e5c4e935d158b85811d33f72d10373d914"><code>8a6df9e</code></a>
ExpressionPropertyCondition constructor should be public</li>
<li><a
href="https://github.com/qos-ch/logback/commit/95e588c4e37b3e76ff2a5c13e60d7e0485d43fb2"><code>95e588c</code></a>
minor changes in ExpressionPropertyCondition</li>
<li><a
href="https://github.com/qos-ch/logback/commit/859f5a1f34cdec0f63a1830394df8238e780a9f4"><code>859f5a1</code></a>
added ExpressionPropertyCondition capable of parsing logical expressions
on p...</li>
<li><a
href="https://github.com/qos-ch/logback/commit/348075adfa7cdd8f7bba60225ec570efb7761d3c"><code>348075a</code></a>
start work on 1.5.24-SNAPSHOT</li>
<li>See full diff in <a
href="https://github.com/qos-ch/logback/compare/v_1.5.23...v_1.5.24">compare
view</a></li>
</ul>
</details>
<br />


Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-22 19:26:24 +00:00
stirlingbot[bot] 58a00dde24 Update Backend 3rd Party Licenses (#5490)
Auto-generated by stirlingbot[bot]

This PR updates the backend license report based on dependency changes.

Signed-off-by: stirlingbot[bot] <stirlingbot[bot]@users.noreply.github.com>
Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com>
2026-01-22 19:26:02 +00:00
Ludy a95583b9e0 Add sync-versions job to GitHub Actions workflow (#5228)
# Description of Changes

This pull request updates the `.github/workflows/build.yml` file to
improve automation and consistency in the build process. The most
significant change is the addition of a new `sync-versions` job that
automatically checks for and synchronizes version differences across key
files, and posts a comment on the pull request if mismatches are
detected. Additionally, there are minor formatting updates for
consistency.

**Automation and version synchronization:**

* Added a new `sync-versions` job that runs after the build, checks for
version mismatches between `build.gradle` and several frontend files,
and posts or updates a comment on the pull request to notify
contributors if differences are found. This job uses a GitHub App bot
for authentication and ensures that versioning remains consistent across
the project.

**Formatting consistency:**

* Updated the YAML formatting for `node-version`, `cache`, and
`python-version` fields to use double quotes for consistency in the
`build.yml` workflow.
[[1]](diffhunk://#diff-5c3fa597431eda03ac3339ae6bf7f05e1a50d6fc7333679ec38e21b337cb6721L150-R151)
[[2]](diffhunk://#diff-5c3fa597431eda03ac3339ae6bf7f05e1a50d6fc7333679ec38e21b337cb6721L244-R338)


<img width="1127" height="401" alt="image"
src="https://github.com/user-attachments/assets/6930a72d-f72b-4909-97e2-51f0e0f61613"
/>


---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2026-01-22 19:23:52 +00:00
Ludy 56f788957a feat(admin): add H2 database backup & restore management to admin UI (#5528)
# Description of Changes

This PR introduces comprehensive database backup and restore management
for embedded H2 databases directly within the admin UI, along with a
corrected backend condition for H2 detection.

### What was changed
- Fixed `H2SQLCondition` logic so embedded H2 is correctly detected when
custom databases are disabled.
- Updated and expanded unit tests to reflect the corrected H2 enablement
behavior.
- Added a new **Backups & Restore** section to the Admin Database
settings UI.
- Implemented UI features to:
  - Create H2 database backups
  - List available backup files with metadata
  - Download, import, and delete existing backups
- Upload and import `.sql` backup files with a confirmation code safety
step
- Added a new frontend service (`databaseManagementService`) to
encapsulate database management API calls.
- Extended English (en-GB) translations for all new database backup and
restore UI elements.

### Why the change was made
- Embedded H2 is the default database when no custom database is
configured, but this was not consistently reflected in backend
conditions.
- Administrators need a safe, UI-driven way to manage H2 backups without
manual filesystem access.
- The new UI improves operability, safety (confirmation code on import),
and transparency (version and status badges).

<img width="1920" height="1080" alt="image"
src="https://github.com/user-attachments/assets/15869b9c-fa86-4b31-ac9b-8e6424138b73"
/>
<img width="1920" height="1032" alt="image"
src="https://github.com/user-attachments/assets/8668dc07-0384-40f0-91e1-c6973c1ce535"
/>


Closes #5515

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2026-01-22 19:16:30 +00:00
Anthony Stirling df1295059a API cleanups (#5537)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2026-01-22 18:19:24 +00:00
853161e891 Fix: Whitelist health endpoints in Security and Enterprise Filter + fix keep original commit (#5494)
Community PR that had a bug, kept original Commit for history with bug
fixed, Merge dont squash merge

# Description of Changes

This PR fixes #5159 by allowing standard health check endpoints to be
accessed without authentication, which is required for container
orchestration (Kubernetes/Docker) liveness and readiness probes.

Changes:

1. **Security Whitelist (RequestUriUtils):** Added `/actuator/health`,
`/healthz`, `/liveness`, and `/readiness` to the public whitelist to
prevent 401 Unauthorized.

2. **Enterprise Filter (EnterpriseEndpointFilter):** Added an exception
for health endpoints. Previously, this filter was aggressively blocking
all /actuator/ requests for non-Pro users, returning 404 Not Found even
if the security config was correct.

**Testing:** Verified locally.

**Before:** `curl http://localhost:8080/actuator/health` returned `401
Unauthorized`.
**After:** curl `http://localhost:8080/actuator/health` returns `200 OK`
and `{"status":"UP"}`.

A sample response from cURL:
After the fix
```bash
$ curl -v http://localhost:8080/actuator/health
```
Now returns:
```bash
* Host localhost:8080 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
*   Trying [::1]:8080...
* Connected to localhost (::1) port 8080
> GET /actuator/health HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/8.5.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Mon, 15 Dec 2025 18:39:18 GMT
< Vary: Origin
< Vary: Access-Control-Request-Method
< Vary: Access-Control-Request-Headers
< X-Request-Id: 775fb1ee-35de-400d-9e8b-bd1805e3c61a
< Content-Type: application/vnd.spring-boot.actuator.v3+json
< X-Content-Type-Options: nosniff
< X-XSS-Protection: 0
< Cache-Control: no-cache, no-store, max-age=0, must-revalidate
< Pragma: no-cache
< Expires: 0
< X-Frame-Options: DENY
< Transfer-Encoding: chunked
<
* Connection #0 to host localhost left intact
{"status":"UP"}
```

---

## Checklist

### General

- [x] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [x] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [x] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [x] I have performed a self-review of my own code
- [x] My changes generate no new warnings

### Documentation
**NOT APPLICABLE**
- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)
**NOT APPLICABLE**
- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [x] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Co-authored-by: Saksham Mittal <saksham2005mittal@gmail.com>
Co-authored-by: Dario Ghunney Ware <dariogware@gmail.com>
2026-01-22 16:14:29 +00:00
Balázs Szücs 64b33ea62b fix(annotations): fix error 300 on sign frontend (#5536)
# Description of Changes

the bug


https://github.com/user-attachments/assets/7f524c95-e57a-4188-a7a9-8d3020a3bb47


<!--


Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [X] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [X] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [X] I have performed a self-review of my own code
- [X] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [X] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>
2026-01-22 16:13:54 +00:00
stirlingbot[bot] 0ca4600371 🤖 format everything with pre-commit by stirlingbot (#5530)
Auto-generated by [create-pull-request][1] with **stirlingbot**

[1]: https://github.com/peter-evans/create-pull-request

Signed-off-by: stirlingbot[bot] <stirlingbot[bot]@users.noreply.github.com>
Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com>
2026-01-21 22:59:05 +00:00
dependabot[bot]andLudy 2d254ef0f6 build(deps): bump actions/checkout from 4.2.2 to 6.0.1 (#5465)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4.2.2
to 6.0.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/checkout/releases">actions/checkout's
releases</a>.</em></p>
<blockquote>
<h2>v6.0.1</h2>
<h2>What's Changed</h2>
<ul>
<li>Update all references from v5 and v4 to v6 by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2314">actions/checkout#2314</a></li>
<li>Add worktree support for persist-credentials includeIf by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2327">actions/checkout#2327</a></li>
<li>Clarify v6 README by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2328">actions/checkout#2328</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/checkout/compare/v6...v6.0.1">https://github.com/actions/checkout/compare/v6...v6.0.1</a></p>
<h2>v6.0.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Update README to include Node.js 24 support details and requirements
by <a href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/2248">actions/checkout#2248</a></li>
<li>Persist creds to a separate file by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2286">actions/checkout#2286</a></li>
<li>v6-beta by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2298">actions/checkout#2298</a></li>
<li>update readme/changelog for v6 by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2311">actions/checkout#2311</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/checkout/compare/v5.0.0...v6.0.0">https://github.com/actions/checkout/compare/v5.0.0...v6.0.0</a></p>
<h2>v6-beta</h2>
<h2>What's Changed</h2>
<p>Updated persist-credentials to store the credentials under
<code>$RUNNER_TEMP</code> instead of directly in the local git
config.</p>
<p>This requires a minimum Actions Runner version of <a
href="https://github.com/actions/runner/releases/tag/v2.329.0">v2.329.0</a>
to access the persisted credentials for <a
href="https://docs.github.com/en/actions/tutorials/use-containerized-services/create-a-docker-container-action">Docker
container action</a> scenarios.</p>
<h2>v5.0.1</h2>
<h2>What's Changed</h2>
<ul>
<li>Port v6 cleanup to v5 by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2301">actions/checkout#2301</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/checkout/compare/v5...v5.0.1">https://github.com/actions/checkout/compare/v5...v5.0.1</a></p>
<h2>v5.0.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Update actions checkout to use node 24 by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2226">actions/checkout#2226</a></li>
<li>Prepare v5.0.0 release by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2238">actions/checkout#2238</a></li>
</ul>
<h2>⚠️ Minimum Compatible Runner Version</h2>
<p><strong>v2.327.1</strong><br />
<a
href="https://github.com/actions/runner/releases/tag/v2.327.1">Release
Notes</a></p>
<p>Make sure your runner is updated to this version or newer to use this
release.</p>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/checkout/compare/v4...v5.0.0">https://github.com/actions/checkout/compare/v4...v5.0.0</a></p>
<h2>v4.3.1</h2>
<h2>What's Changed</h2>
<ul>
<li>Port v6 cleanup to v4 by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2305">actions/checkout#2305</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/actions/checkout/blob/main/CHANGELOG.md">actions/checkout's
changelog</a>.</em></p>
<blockquote>
<h1>Changelog</h1>
<h2>v6.0.2</h2>
<ul>
<li>Fix tag handling: preserve annotations and explicit fetch-tags by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2356">actions/checkout#2356</a></li>
</ul>
<h2>v6.0.1</h2>
<ul>
<li>Add worktree support for persist-credentials includeIf by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2327">actions/checkout#2327</a></li>
</ul>
<h2>v6.0.0</h2>
<ul>
<li>Persist creds to a separate file by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2286">actions/checkout#2286</a></li>
<li>Update README to include Node.js 24 support details and requirements
by <a href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/2248">actions/checkout#2248</a></li>
</ul>
<h2>v5.0.1</h2>
<ul>
<li>Port v6 cleanup to v5 by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2301">actions/checkout#2301</a></li>
</ul>
<h2>v5.0.0</h2>
<ul>
<li>Update actions checkout to use node 24 by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2226">actions/checkout#2226</a></li>
</ul>
<h2>v4.3.1</h2>
<ul>
<li>Port v6 cleanup to v4 by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2305">actions/checkout#2305</a></li>
</ul>
<h2>v4.3.0</h2>
<ul>
<li>docs: update README.md by <a
href="https://github.com/motss"><code>@​motss</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1971">actions/checkout#1971</a></li>
<li>Add internal repos for checking out multiple repositories by <a
href="https://github.com/mouismail"><code>@​mouismail</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1977">actions/checkout#1977</a></li>
<li>Documentation update - add recommended permissions to Readme by <a
href="https://github.com/benwells"><code>@​benwells</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2043">actions/checkout#2043</a></li>
<li>Adjust positioning of user email note and permissions heading by <a
href="https://github.com/joshmgross"><code>@​joshmgross</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2044">actions/checkout#2044</a></li>
<li>Update README.md by <a
href="https://github.com/nebuk89"><code>@​nebuk89</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2194">actions/checkout#2194</a></li>
<li>Update CODEOWNERS for actions by <a
href="https://github.com/TingluoHuang"><code>@​TingluoHuang</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/2224">actions/checkout#2224</a></li>
<li>Update package dependencies by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2236">actions/checkout#2236</a></li>
</ul>
<h2>v4.2.2</h2>
<ul>
<li><code>url-helper.ts</code> now leverages well-known environment
variables by <a href="https://github.com/jww3"><code>@​jww3</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/1941">actions/checkout#1941</a></li>
<li>Expand unit test coverage for <code>isGhes</code> by <a
href="https://github.com/jww3"><code>@​jww3</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1946">actions/checkout#1946</a></li>
</ul>
<h2>v4.2.1</h2>
<ul>
<li>Check out other refs/* by commit if provided, fall back to ref by <a
href="https://github.com/orhantoy"><code>@​orhantoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1924">actions/checkout#1924</a></li>
</ul>
<h2>v4.2.0</h2>
<ul>
<li>Add Ref and Commit outputs by <a
href="https://github.com/lucacome"><code>@​lucacome</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1180">actions/checkout#1180</a></li>
<li>Dependency updates by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>- <a
href="https://redirect.github.com/actions/checkout/pull/1777">actions/checkout#1777</a>,
<a
href="https://redirect.github.com/actions/checkout/pull/1872">actions/checkout#1872</a></li>
</ul>
<h2>v4.1.7</h2>
<ul>
<li>Bump the minor-npm-dependencies group across 1 directory with 4
updates by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1739">actions/checkout#1739</a></li>
<li>Bump actions/checkout from 3 to 4 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1697">actions/checkout#1697</a></li>
<li>Check out other refs/* by commit by <a
href="https://github.com/orhantoy"><code>@​orhantoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1774">actions/checkout#1774</a></li>
<li>Pin actions/checkout's own workflows to a known, good, stable
version. by <a href="https://github.com/jww3"><code>@​jww3</code></a> in
<a
href="https://redirect.github.com/actions/checkout/pull/1776">actions/checkout#1776</a></li>
</ul>
<h2>v4.1.6</h2>
<ul>
<li>Check platform to set archive extension appropriately by <a
href="https://github.com/cory-miller"><code>@​cory-miller</code></a> in
<a
href="https://redirect.github.com/actions/checkout/pull/1732">actions/checkout#1732</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/actions/checkout/commit/8e8c483db84b4bee98b60c0593521ed34d9990e8"><code>8e8c483</code></a>
Clarify v6 README (<a
href="https://redirect.github.com/actions/checkout/issues/2328">#2328</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/033fa0dc0b82693d8986f1016a0ec2c5e7d9cbb1"><code>033fa0d</code></a>
Add worktree support for persist-credentials includeIf (<a
href="https://redirect.github.com/actions/checkout/issues/2327">#2327</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/c2d88d3ecc89a9ef08eebf45d9637801dcee7eb5"><code>c2d88d3</code></a>
Update all references from v5 and v4 to v6 (<a
href="https://redirect.github.com/actions/checkout/issues/2314">#2314</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/1af3b93b6815bc44a9784bd300feb67ff0d1eeb3"><code>1af3b93</code></a>
update readme/changelog for v6 (<a
href="https://redirect.github.com/actions/checkout/issues/2311">#2311</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/71cf2267d89c5cb81562390fa70a37fa40b1305e"><code>71cf226</code></a>
v6-beta (<a
href="https://redirect.github.com/actions/checkout/issues/2298">#2298</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/069c6959146423d11cd0184e6accf28f9d45f06e"><code>069c695</code></a>
Persist creds to a separate file (<a
href="https://redirect.github.com/actions/checkout/issues/2286">#2286</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493"><code>ff7abcd</code></a>
Update README to include Node.js 24 support details and requirements (<a
href="https://redirect.github.com/actions/checkout/issues/2248">#2248</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/08c6903cd8c0fde910a37f88322edcfb5dd907a8"><code>08c6903</code></a>
Prepare v5.0.0 release (<a
href="https://redirect.github.com/actions/checkout/issues/2238">#2238</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/9f265659d3bb64ab1440b03b12f4d47a24320917"><code>9f26565</code></a>
Update actions checkout to use node 24 (<a
href="https://redirect.github.com/actions/checkout/issues/2226">#2226</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/08eba0b27e820071cde6df949e0beb9ba4906955"><code>08eba0b</code></a>
Prepare release v4.3.0 (<a
href="https://redirect.github.com/actions/checkout/issues/2237">#2237</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/actions/checkout/compare/v4.2.2...8e8c483db84b4bee98b60c0593521ed34d9990e8">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/checkout&package-manager=github_actions&previous-version=4.2.2&new-version=6.0.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ludy <Ludy87@users.noreply.github.com>
2026-01-21 22:26:01 +00:00
dependabot[bot] a28464a954 build(deps): bump reportlab from 4.4.5 to 4.4.9 in /testing/cucumber (#5505)
Bumps [reportlab](https://www.reportlab.com/) from 4.4.5 to 4.4.9.


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=reportlab&package-manager=pip&previous-version=4.4.5&new-version=4.4.9)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-21 22:24:58 +00:00
dependabot[bot] fe56831889 build(deps-dev): bump stylelint-config-standard from 39.0.1 to 40.0.0 in /devTools (#5504)
Bumps
[stylelint-config-standard](https://github.com/stylelint/stylelint-config-standard)
from 39.0.1 to 40.0.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/stylelint/stylelint-config-standard/releases">stylelint-config-standard's
releases</a>.</em></p>
<blockquote>
<h2>40.0.0</h2>
<ul>
<li>Removed: <code>stylelint</code> less than <code>17.0.0</code> from
peer dependencies.</li>
<li>Removed: support for Node.js less than <code>20.19.0</code>.</li>
<li>Changed: updated to <a
href="https://github.com/stylelint/stylelint-config-recommended/releases/tag/18.0.0"><code>stylelint-config-recommended@18.0.0</code></a>.</li>
<li>Changed: module type to ESM.</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/stylelint/stylelint-config-standard/blob/main/CHANGELOG.md">stylelint-config-standard's
changelog</a>.</em></p>
<blockquote>
<h2>40.0.0</h2>
<ul>
<li>Removed: <code>stylelint</code> less than <code>17.0.0</code> from
peer dependencies.</li>
<li>Removed: support for Node.js less than <code>20.19.0</code>.</li>
<li>Changed: updated to <a
href="https://github.com/stylelint/stylelint-config-recommended/releases/tag/18.0.0"><code>stylelint-config-recommended@18.0.0</code></a>.</li>
<li>Changed: module type to ESM.</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/stylelint/stylelint-config-standard/commit/fbbe7b8e3efaf6ad84084268944be412e4bee3df"><code>fbbe7b8</code></a>
Release 40.0.0 (<a
href="https://redirect.github.com/stylelint/stylelint-config-standard/issues/386">#386</a>)</li>
<li><a
href="https://github.com/stylelint/stylelint-config-standard/commit/e6368f9a6d83e178b97e3cf8eb1f76039f35df67"><code>e6368f9</code></a>
Prepare 40.0.0 (<a
href="https://redirect.github.com/stylelint/stylelint-config-standard/issues/381">#381</a>)</li>
<li><a
href="https://github.com/stylelint/stylelint-config-standard/commit/481e21c6ee154cb43b34c5b406e75b3a72cbfbc3"><code>481e21c</code></a>
Bump the dev-deps group with 3 updates (<a
href="https://redirect.github.com/stylelint/stylelint-config-standard/issues/382">#382</a>)</li>
<li><a
href="https://github.com/stylelint/stylelint-config-standard/commit/696689d525f3196bd90d4a58bb625cf56e6b67ba"><code>696689d</code></a>
Bump stylelint/.github/.github/workflows/call-release-pr.yml from 0.3.1
to 0....</li>
<li><a
href="https://github.com/stylelint/stylelint-config-standard/commit/5d46c0f12b5ab9db4489508b1b58fea72d30a19c"><code>5d46c0f</code></a>
Bump stylelint/.github/.github/workflows/call-lint.yml from 0.3.1 to
0.5.0 (#...</li>
<li><a
href="https://github.com/stylelint/stylelint-config-standard/commit/f888c90852fe7295926775580c1953d5c2d4580d"><code>f888c90</code></a>
Bump stylelint/.github/.github/workflows/call-test.yml from 0.3.1 to
0.5.0 (#...</li>
<li><a
href="https://github.com/stylelint/stylelint-config-standard/commit/066dda1ba5047a6918141220ab0ef89247f93bf7"><code>066dda1</code></a>
Fix vulnerable dependencies via <code>npm audit fix</code> (<a
href="https://redirect.github.com/stylelint/stylelint-config-standard/issues/378">#378</a>)</li>
<li><a
href="https://github.com/stylelint/stylelint-config-standard/commit/b6e6aeceddacb35e781ebd1fb4e6c88d12aeb5cd"><code>b6e6aec</code></a>
Bump js-yaml from 4.1.0 to 4.1.1 (<a
href="https://redirect.github.com/stylelint/stylelint-config-standard/issues/379">#379</a>)</li>
<li><a
href="https://github.com/stylelint/stylelint-config-standard/commit/63bb8ddee460919d160b4bd15b8442c6201c6096"><code>63bb8dd</code></a>
Bump mdast-util-to-hast from 13.1.0 to 13.2.1 (<a
href="https://redirect.github.com/stylelint/stylelint-config-standard/issues/380">#380</a>)</li>
<li><a
href="https://github.com/stylelint/stylelint-config-standard/commit/0cc790a91bb5ba1fc29a83fe4f7156965fad23df"><code>0cc790a</code></a>
Bump the dev-deps group with 2 updates (<a
href="https://redirect.github.com/stylelint/stylelint-config-standard/issues/377">#377</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/stylelint/stylelint-config-standard/compare/39.0.1...40.0.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=stylelint-config-standard&package-manager=npm_and_yarn&previous-version=39.0.1&new-version=40.0.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-21 22:24:29 +00:00
dependabot[bot] f5fdb870a8 build(deps): bump actions/download-artifact from 6.0.0 to 7.0.0 (#5503)
Bumps
[actions/download-artifact](https://github.com/actions/download-artifact)
from 6.0.0 to 7.0.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/download-artifact/releases">actions/download-artifact's
releases</a>.</em></p>
<blockquote>
<h2>v7.0.0</h2>
<h2>v7 - What's new</h2>
<blockquote>
<p>[!IMPORTANT]
actions/download-artifact@v7 now runs on Node.js 24 (<code>runs.using:
node24</code>) and requires a minimum Actions Runner version of 2.327.1.
If you are using self-hosted runners, ensure they are updated before
upgrading.</p>
</blockquote>
<h3>Node.js 24</h3>
<p>This release updates the runtime to Node.js 24. v6 had preliminary
support for Node 24, however this action was by default still running on
Node.js 20. Now this action by default will run on Node.js 24.</p>
<h2>What's Changed</h2>
<ul>
<li>Update GHES guidance to include reference to Node 20 version by <a
href="https://github.com/patrikpolyak"><code>@​patrikpolyak</code></a>
in <a
href="https://redirect.github.com/actions/download-artifact/pull/440">actions/download-artifact#440</a></li>
<li>Download Artifact Node24 support by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/download-artifact/pull/415">actions/download-artifact#415</a></li>
<li>fix: update <code>@​actions/artifact</code> to fix Node.js 24
punycode deprecation by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/download-artifact/pull/451">actions/download-artifact#451</a></li>
<li>prepare release v7.0.0 for Node.js 24 support by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/download-artifact/pull/452">actions/download-artifact#452</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/patrikpolyak"><code>@​patrikpolyak</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/download-artifact/pull/440">actions/download-artifact#440</a></li>
<li><a href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/download-artifact/pull/415">actions/download-artifact#415</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/download-artifact/compare/v6.0.0...v7.0.0">https://github.com/actions/download-artifact/compare/v6.0.0...v7.0.0</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/actions/download-artifact/commit/37930b1c2abaa49bbe596cd826c3c89aef350131"><code>37930b1</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/download-artifact/issues/452">#452</a>
from actions/download-artifact-v7-release</li>
<li><a
href="https://github.com/actions/download-artifact/commit/72582b9e0acd370909e83fa4a1fd0fca3ad452d8"><code>72582b9</code></a>
doc: update readme</li>
<li><a
href="https://github.com/actions/download-artifact/commit/0d2ec9d4cbcefe257d822f108de2a1f15f8da9f6"><code>0d2ec9d</code></a>
chore: release v7.0.0 for Node.js 24 support</li>
<li><a
href="https://github.com/actions/download-artifact/commit/fd7ae8fda6dc16277a9ffbc91cdb0eedf156e912"><code>fd7ae8f</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/download-artifact/issues/451">#451</a>
from actions/fix-storage-blob</li>
<li><a
href="https://github.com/actions/download-artifact/commit/d484700543354b15886d6a52910cf61b7f1d2b27"><code>d484700</code></a>
chore: restore minimatch.dep.yml license file</li>
<li><a
href="https://github.com/actions/download-artifact/commit/03a808050efe42bb6ad85281890afd4e4546672c"><code>03a8080</code></a>
chore: remove obsolete dependency license files</li>
<li><a
href="https://github.com/actions/download-artifact/commit/56fe6d904b0968950f8b68ea17774c54973ed5e2"><code>56fe6d9</code></a>
chore: update <code>@​actions/artifact</code> license file to 5.0.1</li>
<li><a
href="https://github.com/actions/download-artifact/commit/8e3ebc4ab4d2e095e5eb44ba1a4a53b6b03976ad"><code>8e3ebc4</code></a>
chore: update package-lock.json with <code>@​actions/artifact</code><a
href="https://github.com/5"><code>@​5</code></a>.0.1</li>
<li><a
href="https://github.com/actions/download-artifact/commit/1e3c4b4d4906c98ab57453c24efefdf16c078044"><code>1e3c4b4</code></a>
fix: update <code>@​actions/artifact</code> to ^5.0.0 for Node.js 24
punycode fix</li>
<li><a
href="https://github.com/actions/download-artifact/commit/458627d354794c71bc386c8d5839d20b5885fe2a"><code>458627d</code></a>
chore: use local <code>@​actions/artifact</code> package for Node.js 24
testing</li>
<li>Additional commits viewable in <a
href="https://github.com/actions/download-artifact/compare/018cc2cf5baa6db3ef3c5f8a56943fffe632ef53...37930b1c2abaa49bbe596cd826c3c89aef350131">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/download-artifact&package-manager=github_actions&previous-version=6.0.0&new-version=7.0.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-21 22:24:14 +00:00
albanobattistella 2befd2f6e9 Update Italian translations (#5508)
Updated Italian translations for various UI elements and descriptions.

# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [x] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2026-01-21 22:11:58 +00:00
Stephan PaternotteandLudy a2f6acd732 chore(i18n): update and expand Dutch (nl-NL) translations (#5355)
last week's updates translated plus a number of corrections and solving
inconsistencies

---------

Co-authored-by: Ludy <Ludy87@users.noreply.github.com>
2026-01-21 22:11:49 +00:00
LudyandCopilot 1436821a3a feat(admin): add tessdata language management for OCR and download support (#5519)
# Description of Changes

### What was changed
- Added new admin-only API endpoints to:
  - List installed tessdata OCR languages
- Fetch available tessdata languages from the official Tesseract
repository
- Download selected tessdata language files directly into the configured
tessdata directory
- Implemented server-side validation, safe language name handling, and
directory writability checks.
- Extended the Admin Advanced Settings UI to:
  - Display installed tessdata languages
  - Show available remote languages not yet installed
- Allow selecting and downloading additional languages via a
multi-select UI
- Gracefully fall back to manual download links when the tessdata
directory is not writable
- Added new i18n strings for all related UI states (loading, success,
error, permission warnings).

### Why the change was made
- Managing OCR languages previously required manual filesystem
interaction.
- This change improves usability for administrators by enabling in-app
management of tessdata languages while maintaining security constraints.
- The writable directory check and manual fallback ensure compatibility
with restricted or containerized environments.


<img width="1282" height="832" alt="image"
src="https://github.com/user-attachments/assets/aa958730-0ffb-4fd6-9af8-87c527a476e4"
/>


---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-21 22:10:47 +00:00
Ludy 23f872823d fix(pipeline): avoid bad multipart by letting RestTemplate set boundary (#5522)
# Description of Changes

- **What was changed**
- Updated `PipelineProcessor#sendWebRequest(...)` to stop forcing
`Content-Type: multipart/form-data` and to stop manually writing the
multipart body via `FormHttpMessageConverter`.
- Switched to `RestTemplate#httpEntityCallback(...)` (`RequestCallback`)
so Spring’s configured message converters handle multipart serialization
(including boundary generation) correctly.
- Made `X-API-KEY` header conditional (only added when
non-null/non-empty).
- Added a unit test (`sendWebRequestDoesNotForceContentType`) to ensure
`Content-Type` is not explicitly set on the outgoing request headers and
that the request succeeds end-to-end using mocked `RestTemplate`
behavior.

- **Why the change was made**
- The server-side error `org.eclipse.jetty.http.BadMessageException:
400: bad multipart` with `EOFException: unexpected EOF` indicates the
multipart request can be malformed (commonly due to
boundary/content-type mismatches or prematurely terminated streams).
Manually setting the multipart content type and writing the body can
bypass Spring’s normal boundary handling and lead to invalid multipart
payloads. Letting `RestTemplate` handle it ensures correct formatting.

---

This pull request updates how multipart web requests are sent in the
pipeline processor to avoid forcing the `Content-Type` header for
multipart requests, allowing the message converter to set it
automatically. It also adds a new test to ensure this behavior. The main
changes are focused on improving multipart request handling and
increasing test coverage.

**Multipart request handling improvements:**

* Refactored `PipelineProcessor.runPipelineAgainstFiles` to avoid
explicitly setting the `Content-Type` header to `multipart/form-data`,
letting the message converter set the appropriate boundary and content
type instead. This helps prevent issues with incorrect boundaries and
improves compatibility with multipart requests.
* Updated imports in `PipelineProcessor.java` to remove unused
`FormHttpMessageConverter` and add `RequestCallback`.

**Testing enhancements:**

* Added a new test `sendWebRequestDoesNotForceContentType` in
`PipelineProcessorTest.java` to verify that the `Content-Type` header is
not set explicitly and is left for the message converter to handle. This
test uses mocking to simulate the request and response flow.
* Added necessary imports in `PipelineProcessorTest.java` to support new
test logic and mocking.
[[1]](diffhunk://#diff-1a45e7b37023bd3a9580ab24d7025bb359cb8891c486111d372c0557f817b3edR7-R8)
[[2]](diffhunk://#diff-1a45e7b37023bd3a9580ab24d7025bb359cb8891c486111d372c0557f817b3edR18-R27)

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2026-01-21 22:00:59 +00:00
Ludy 80cba55459 refactor: remove legacy Thymeleaf web UI controllers and templates (#5406)
# Description of Changes

This pull request completes the removal of Thymeleaf template engine
support and documentation from the Stirling-PDF codebase and developer
guides, reflecting the project's full migration to a React-based
frontend. It removes all references to Thymeleaf in code, configuration,
and documentation, and updates guides to focus exclusively on the React
SPA architecture. This streamlines the codebase and clarifies the
development workflow for contributors.

**Codebase cleanup: Thymeleaf removal**
- Removed all commented-out Thymeleaf dependencies and related
configuration beans from `build.gradle` and `AppConfig.java`
(`app/common/build.gradle`,
`app/common/src/main/java/stirling/software/common/configuration/AppConfig.java`)
[[1]](diffhunk://#diff-2a1a21726f33b05d16451237c68d6df91a5f4a58419d839715f3f1538a9a14aeL32)
[[2]](diffhunk://#diff-70792df9a0ab5675ded888c9eb8e2815c780d7b39f4bda8cf2da51d1b336899aL67-L76).
- Fully commented out (as a precursor to future deletion) the
`FileFallbackTemplateResolver` and `InputStreamTemplateResource`
classes, which were only used for Thymeleaf template resolution
(`app/common/src/main/java/stirling/software/common/configuration/FileFallbackTemplateResolver.java`,
`app/common/src/main/java/stirling/software/common/model/InputStreamTemplateResource.java`)
[[1]](diffhunk://#diff-e2bc7614074316b972355cb7dda47b98f75b00eb6b2ca4f143a680ab2803dcd8L1-L49)
[[2]](diffhunk://#diff-ab10ee12d8de8fb77759e931170373d388bde04bad6d0e42a0ab674355ef7ef3L1-L40).

**Documentation updates: React-only focus**
- Removed all instructions and references to migrating or developing
with Thymeleaf templates in `DeveloperGuide.md` and `ADDING_TOOLS.md`,
including detailed Thymeleaf usage examples, migration steps, and
translation key usage in templates
[[1]](diffhunk://#diff-ccd22fcbec8148152c8c77b85fbfe2633a6707b5ad50c2ef88fa87e2c47ea88fL5-R5)
[[2]](diffhunk://#diff-ccd22fcbec8148152c8c77b85fbfe2633a6707b5ad50c2ef88fa87e2c47ea88fL41-L43)
[[3]](diffhunk://#diff-ccd22fcbec8148152c8c77b85fbfe2633a6707b5ad50c2ef88fa87e2c47ea88fL103-L105)
[[4]](diffhunk://#diff-ccd22fcbec8148152c8c77b85fbfe2633a6707b5ad50c2ef88fa87e2c47ea88fL157)
[[5]](diffhunk://#diff-ccd22fcbec8148152c8c77b85fbfe2633a6707b5ad50c2ef88fa87e2c47ea88fL312)
[[6]](diffhunk://#diff-ccd22fcbec8148152c8c77b85fbfe2633a6707b5ad50c2ef88fa87e2c47ea88fL404-R396)
[[7]](diffhunk://#diff-ccd22fcbec8148152c8c77b85fbfe2633a6707b5ad50c2ef88fa87e2c47ea88fL451-L505)
[[8]](diffhunk://#diff-ccd22fcbec8148152c8c77b85fbfe2633a6707b5ad50c2ef88fa87e2c47ea88fL530-R467)
[[9]](diffhunk://#diff-ccd22fcbec8148152c8c77b85fbfe2633a6707b5ad50c2ef88fa87e2c47ea88fL585-L669)
[[10]](diffhunk://#diff-ccd22fcbec8148152c8c77b85fbfe2633a6707b5ad50c2ef88fa87e2c47ea88fL699-L709)
[[11]](diffhunk://#diff-e2f8148ea620602b7761e8ee24afeac1c577476630528e210fe0b22e950016ddL3-R3)
[[12]](diffhunk://#diff-e2f8148ea620602b7761e8ee24afeac1c577476630528e210fe0b22e950016ddL267-R267).
- Updated architecture descriptions in `CLAUDE.md` to reflect that the
frontend is now exclusively a React SPA and that Thymeleaf templates
have been fully replaced
[[1]](diffhunk://#diff-6ebdb617a8104a7756d0cf36578ab01103dc9f07e4dc6feb751296b9c402faf7L131-R132)
[[2]](diffhunk://#diff-6ebdb617a8104a7756d0cf36578ab01103dc9f07e4dc6feb751296b9c402faf7L143-L144).

**Labeler configuration update**
- Removed labeler rules for files related to the old Thymeleaf-based web
controllers and UI directories, as these are now obsolete
(`.github/labeler-config-srvaroa.yml`).

These changes ensure the codebase and documentation are consistent with
the new React-only frontend approach, reducing maintenance overhead and
potential confusion for contributors.

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2026-01-21 21:58:29 +00:00
Balázs Szücs b58efaf388 fix(automation): add zIndex and withinPortal props to dropdown components for improved UI consistency (#5527)
# Description of Changes
Closes: #5525
Closes: #5242


<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [X] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [X] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [X] I have performed a self-review of my own code
- [X] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [X] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>
2026-01-21 21:54:13 +00:00
Anthony Stirling 68df661204 Fix hook ordering for endpoint name resolution (#5517)
### Motivation
- Prevent a React hook ordering issue by removing conditional
`useCallback` usage in endpoint name resolution within the base
parameters hook.

### Description
- Replace conditional creation of `getEndpointName` with a single
unconditional `useCallback` that reads `config.endpointName` and returns
either the static string or calls the function with `parameters`,
updating `frontend/src/core/hooks/tools/shared/useBaseParameters.ts`.

### Testing
- No automated tests were executed in this change; please run `./gradlew
build` (or confirm CI build) to verify the project compiles
successfully.

------
[Codex
Task](https://chatgpt.com/codex/tasks/task_b_696ec2829624832898fb4a92192ed548)
2026-01-21 21:40:02 +00:00
Anthony Stirling 20f984156f scrollbar fixes (#5529)
# Description of Changes

- Add scrolling bar to settings menus
- Fix scrolling bar in My files

-fix depreciated pdf editor code


## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2026-01-21 21:35:17 +00:00
Balázs SzücsandReece Browne bd75ad042a [V2] fix(automation): enhance parameter handling and default values across operations, fix error in ManyToOne tools (#5123)
# Description of Changes
This pull request focuses on improving the handling of tool parameters
in the frontend automation and tool operation system. The main changes
ensure that default parameter values are consistently applied, missing
or undefined values are handled gracefully, and custom tools like
Automate and Merge correctly track input file consumption. These updates
enhance reliability and reduce errors due to missing or improperly
formatted parameters.

**Parameter Handling and Defaults:**

* Tool configuration and automation forms now merge user-provided
parameters with tool-specific default parameters, guaranteeing all
required fields are present during initialization and execution
(`ToolConfigurationModal.tsx`, `useAutomationForm.ts`,
`automationExecutor.ts`).
* All tool operation hooks (e.g., Add Watermark, Merge, Sanitize, Split,
Compress, Change Permissions, OCR) have been updated to safely handle
missing or undefined parameter values by providing sensible defaults,
preventing runtime errors and backend issues
(`useAddWatermarkOperation.ts`, `useMergeOperation.ts`,
`useSanitizeOperation.ts`, `useSplitOperation.ts`,
`useCompressOperation.ts`, `useChangePermissionsOperation.ts`,
`useOCROperation.ts`).

**Tool Operation Configuration Enhancements:**

* The `BaseToolOperationConfig` interface now supports a
`consumesAllInputs` flag, allowing tools with complex input-output
relationships (like Automate and Merge) to mark all input files as
successfully processed when appropriate (`useToolOperation.ts`).
* The Automate tool operation is configured to use this new flag, and
the core hook logic is updated to respect it when determining success
(`useAutomateOperation.ts`, `useToolOperation.ts`).

**Registry and Configuration Consistency:**

* Default parameters are now explicitly included in tool operation
configs (e.g., Merge), ensuring consistency between registry definitions
and runtime behavior (`useMergeOperation.ts`).

**Dependency Updates:**

* React hooks and callbacks now correctly include dependencies related
to parameter defaults to avoid stale values in forms and modals
(`useAutomationForm.ts`).

These changes collectively improve the robustness and maintainability of
the automation and tool configuration system.


<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [X] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [X] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [X] I have performed a self-review of my own code
- [X] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [X] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>
Co-authored-by: Reece Browne <74901996+reecebrowne@users.noreply.github.com>
2026-01-20 14:57:44 +00:00
Ludy 8bb807471f chore(ci): enable Gradle dependency caching across GitHub workflows (#5400)
# Description of Changes

This pull request updates the CI/CD workflows and Gradle configuration
to improve build reproducibility, security, and external dependency
management. The main changes include standardizing Gradle setup across
workflows, securely injecting Maven credentials, and enabling Gradle
build caching. There are also minor improvements to dependency version
management and plugin repository configuration.

**CI/CD Workflow Improvements:**

- Standardized Gradle setup across all GitHub Actions workflows by
explicitly adding a `Setup Gradle` step using
`gradle/actions/setup-gradle@v5.0.0` and specifying Gradle version 8.14.
This replaces previous usages and ensures consistency.
[[1]](diffhunk://#diff-5c3fa597431eda03ac3339ae6bf7f05e1a50d6fc7333679ec38e21b337cb6721R71-R81)
[[2]](diffhunk://#diff-8d23782ae5caff72d55828bb25814854f5f2523f299d7dbcda4a3537dd84c5c3L157-R176)
[[3]](diffhunk://#diff-5c3fa597431eda03ac3339ae6bf7f05e1a50d6fc7333679ec38e21b337cb6721R134-R144)
[[4]](diffhunk://#diff-5c3fa597431eda03ac3339ae6bf7f05e1a50d6fc7333679ec38e21b337cb6721R206-R216)
[[5]](diffhunk://#diff-5c3fa597431eda03ac3339ae6bf7f05e1a50d6fc7333679ec38e21b337cb6721R260-R264)
[[6]](diffhunk://#diff-5c3fa597431eda03ac3339ae6bf7f05e1a50d6fc7333679ec38e21b337cb6721R331-R341)
[[7]](diffhunk://#diff-3c0f521958c53ad27c967692b4d5480ead136acb33622ee97d39df814b1b202eR339-R351)
[[8]](diffhunk://#diff-895b214ee023c8c26048a2a3b946cfb1ebc4f26fbc8a9c2fa54b77c12e763b6bL53-R54)
[[9]](diffhunk://#diff-895b214ee023c8c26048a2a3b946cfb1ebc4f26fbc8a9c2fa54b77c12e763b6bL121-R127)
[[10]](diffhunk://#diff-895b214ee023c8c26048a2a3b946cfb1ebc4f26fbc8a9c2fa54b77c12e763b6bR206-R217)
[[11]](diffhunk://#diff-6a2e9fb077e57351f4a7e10d03b114e256298babdf06e7e7ae666781a5cf36a1R60-R70)
[[12]](diffhunk://#diff-62dcbe64a950b4efb54d691e1e87451a8cd535400aa9ea1e40893de5b57cd73bL45-R46)
[[13]](diffhunk://#diff-76056236de05155107f6a660f1e3956059e37338011b8f0e72188afcb9b17b6fL46-R56)
[[14]](diffhunk://#diff-fd60dc2adec58c1005c4e4164e9c24362fd6082fd3ab0403e54d276d9835fa6eL42-R65)
[[15]](diffhunk://#diff-b34ab107dd4bc92075b2e89b6f16e4a2813e267ca7c2afebdb1931a0a3900d5aR102-R114)
[[16]](diffhunk://#diff-98b618771a57e1758961359ecacbac2cff7cfef29aa021c3bc294ae926c4ce5bL47-R51)

- Enabled Gradle build cache (`--build-cache`) for all build-related
commands in workflows, improving build performance and consistency. Also
removed unnecessary `clean` commands before builds to further optimize
workflow times.
[[1]](diffhunk://#diff-5c3fa597431eda03ac3339ae6bf7f05e1a50d6fc7333679ec38e21b337cb6721R71-R81)
[[2]](diffhunk://#diff-8d23782ae5caff72d55828bb25814854f5f2523f299d7dbcda4a3537dd84c5c3L157-R176)
[[3]](diffhunk://#diff-5c3fa597431eda03ac3339ae6bf7f05e1a50d6fc7333679ec38e21b337cb6721R134-R144)
[[4]](diffhunk://#diff-5c3fa597431eda03ac3339ae6bf7f05e1a50d6fc7333679ec38e21b337cb6721R206-R216)
[[5]](diffhunk://#diff-5c3fa597431eda03ac3339ae6bf7f05e1a50d6fc7333679ec38e21b337cb6721R331-R341)
[[6]](diffhunk://#diff-3c0f521958c53ad27c967692b4d5480ead136acb33622ee97d39df814b1b202eR339-R351)
[[7]](diffhunk://#diff-895b214ee023c8c26048a2a3b946cfb1ebc4f26fbc8a9c2fa54b77c12e763b6bL134-R144)
[[8]](diffhunk://#diff-895b214ee023c8c26048a2a3b946cfb1ebc4f26fbc8a9c2fa54b77c12e763b6bR206-R217)
[[9]](diffhunk://#diff-6a2e9fb077e57351f4a7e10d03b114e256298babdf06e7e7ae666781a5cf36a1R60-R70)
[[10]](diffhunk://#diff-76056236de05155107f6a660f1e3956059e37338011b8f0e72188afcb9b17b6fL46-R56)
[[11]](diffhunk://#diff-fd60dc2adec58c1005c4e4164e9c24362fd6082fd3ab0403e54d276d9835fa6eL42-R65)
[[12]](diffhunk://#diff-b34ab107dd4bc92075b2e89b6f16e4a2813e267ca7c2afebdb1931a0a3900d5aR102-R114)
[[13]](diffhunk://#diff-98b618771a57e1758961359ecacbac2cff7cfef29aa021c3bc294ae926c4ce5bL47-R51)

**Security and Dependency Management:**

- Injected Maven credentials (`MAVEN_USER`, `MAVEN_PASSWORD`,
`MAVEN_PUBLIC_URL`) as environment variables in all relevant workflow
steps, supporting secure access to private or custom Maven repositories.
[[1]](diffhunk://#diff-5c3fa597431eda03ac3339ae6bf7f05e1a50d6fc7333679ec38e21b337cb6721R71-R81)
[[2]](diffhunk://#diff-8d23782ae5caff72d55828bb25814854f5f2523f299d7dbcda4a3537dd84c5c3L157-R176)
[[3]](diffhunk://#diff-5c3fa597431eda03ac3339ae6bf7f05e1a50d6fc7333679ec38e21b337cb6721R134-R144)
[[4]](diffhunk://#diff-5c3fa597431eda03ac3339ae6bf7f05e1a50d6fc7333679ec38e21b337cb6721R206-R216)
[[5]](diffhunk://#diff-5c3fa597431eda03ac3339ae6bf7f05e1a50d6fc7333679ec38e21b337cb6721R290-R293)
[[6]](diffhunk://#diff-5c3fa597431eda03ac3339ae6bf7f05e1a50d6fc7333679ec38e21b337cb6721R331-R341)
[[7]](diffhunk://#diff-3c0f521958c53ad27c967692b4d5480ead136acb33622ee97d39df814b1b202eR339-R351)
[[8]](diffhunk://#diff-895b214ee023c8c26048a2a3b946cfb1ebc4f26fbc8a9c2fa54b77c12e763b6bR66-R69)
[[9]](diffhunk://#diff-895b214ee023c8c26048a2a3b946cfb1ebc4f26fbc8a9c2fa54b77c12e763b6bL134-R144)
[[10]](diffhunk://#diff-895b214ee023c8c26048a2a3b946cfb1ebc4f26fbc8a9c2fa54b77c12e763b6bR281-R283)
[[11]](diffhunk://#diff-62dcbe64a950b4efb54d691e1e87451a8cd535400aa9ea1e40893de5b57cd73bR57-R60)
[[12]](diffhunk://#diff-76056236de05155107f6a660f1e3956059e37338011b8f0e72188afcb9b17b6fR73-R76)
[[13]](diffhunk://#diff-fd60dc2adec58c1005c4e4164e9c24362fd6082fd3ab0403e54d276d9835fa6eL42-R65)
[[14]](diffhunk://#diff-b34ab107dd4bc92075b2e89b6f16e4a2813e267ca7c2afebdb1931a0a3900d5aR178-R180)
[[15]](diffhunk://#diff-98b618771a57e1758961359ecacbac2cff7cfef29aa021c3bc294ae926c4ce5bL47-R51)

- Added a `pluginManagement` block in `settings.gradle` to allow Gradle
plugins to be resolved from a custom Maven repository if specified by
environment variables, increasing flexibility for plugin sourcing.

**Build and Dependency Versioning:**

- Updated `app/proprietary/build.gradle` to use the
`bouncycastleVersion` variable for the Bouncy Castle dependency version,
improving maintainability and consistency of dependency versioning.

**Workflow Trigger Improvements:**

- Expanded the file path triggers in
`.github/workflows/sync_files_v2.yml` to include additional Gradle build
files, ensuring the workflow runs when any core build files are changed.

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2026-01-19 19:03:50 +00:00
Balázs Szücs 59d816bfa7 feat(pipeline): improve file processing with resource management and temp file handling (#5488)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>
2026-01-17 19:44:02 +00:00
stirlingbot[bot] 473021a13c 🤖 format everything with pre-commit by stirlingbot (#5484)
Auto-generated by [create-pull-request][1] with **stirlingbot**

[1]: https://github.com/peter-evans/create-pull-request

Signed-off-by: stirlingbot[bot] <stirlingbot[bot]@users.noreply.github.com>
Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com>
2026-01-17 00:12:06 +00:00
stirlingbot[bot] f616486bee 🌐 Sync Translations + Update README Progress Table (#5489)
### Description of Changes

This Pull Request was automatically generated to synchronize updates to
translation files and documentation. Below are the details of the
changes made:

#### **1. Synchronization of Translation Files**
- Updated translation files
(`frontend/public/locales/*/translation.toml`) to reflect changes in the
reference file `en-GB/translation.toml`.
- Ensured consistency and synchronization across all supported language
files.
- Highlighted any missing or incomplete translations.
- **Format**: TOML

#### **2. Update README.md**
- Generated the translation progress table in `README.md` using
`counter_translation_v3.py`.
- Added a summary of the current translation status for all supported
languages.
- Included up-to-date statistics on translation coverage.

#### **Why these changes are necessary**
- Keeps translation files aligned with the latest reference updates.
- Ensures the documentation reflects the current translation progress.

---

Auto-generated by [create-pull-request][1].

[1]: https://github.com/peter-evans/create-pull-request

Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com>
2026-01-16 21:01:11 +00:00
Balázs Szücs cb5c2a5803 feat(conversion): add SVG to PDF conversion functionality (#5431)
# Description of Changes


This pull request introduces a new SVG to PDF conversion feature,
including both backend and frontend changes. The backend adds a secure,
vector-preserving SVG-to-PDF conversion endpoint with comprehensive SVG
sanitization to prevent XSS and SSRF attacks. The frontend is updated to
route SVG-to-PDF conversions through this new endpoint and to
distinguish SVG from other image formats. Additionally, a new dependency
is added for PDF rendering.

**Backend: SVG to PDF Conversion and Security**

* Adds a new API endpoint and controller (`ConvertSvgToPDF`) for
converting SVG files to PDF, using Batik and PDFBox with vector graphics
preservation and robust error handling.
* Implements SVG sanitization (`SvgSanitizer`) to remove scripts, event
handlers, and dangerous URLs, protecting against XSS and SSRF attacks.
* Introduces a utility (`SvgToPdf`) for rendering SVG to PDF with
timeout protection against resource exhaustion attacks.
* Defines a new request model (`SvgToPdfRequest`) for SVG to PDF
conversion requests.
* Adds the `pdfbox-graphics2d` dependency for vector graphics PDF
rendering.

**Frontend: Routing and Format Handling**

* Updates conversion endpoint constants to add `svg-pdf` and maps SVG
files to use the new `svg-to-pdf` route instead of the generic
image-to-PDF route.
* Removes SVG from the generic image format list and introduces a
dedicated check for SVG format (`isSvgFormat`).
<img width="1133" height="995" alt="image"
src="https://github.com/user-attachments/assets/dec8cf27-ccb9-490d-af76-bff69feb0423"
/>

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [X] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [X] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [X] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [X] I have performed a self-review of my own code
- [X] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [X] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>
2026-01-16 18:45:50 +00:00
Anthony Stirling 3e061516a5 Libre threads (#5303)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2026-01-15 19:14:45 +00:00
Anthony Stirling 8afbd0afed Docker use latest libreoffice and fonts (#5482)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2026-01-15 15:01:10 +00:00
Krishna Prem 884801ea36 fix: update broken enterprise docs link from /Pro to /Paid-Offerings (#5471)
## Description
Fixed broken documentation link in the air-gapped setup guide that was
pointing to a non-existent `/Pro/#activation` page (returning 404).
Updated to the correct `/Paid-Offerings/#activating-your-license` URL
which returns 200 OK.

## Changes Made
- Updated link in `frontend/src/core/data/useTranslatedToolRegistry.tsx`
(line 838)
- Changed from `https://docs.stirlingpdf.com/Pro/#activation` to
`https://docs.stirlingpdf.com/Paid-Offerings/#activating-your-license`

## Testing
- Verified old link returns 404
- Verified new link returns 200 and has activation section
- Link now correctly points to the "Activating Your License" section of
the Paid Offerings documentation

## Related Issue
Resolves #5088

## Checklist
- [x] Code follows project style guidelines
- [x] Changes are limited to fixing the broken link only (atomic commit)
- [x] Link has been manually tested and verified working
2026-01-15 12:48:34 +00:00
Reece Browne 8fcee482f0 Make controls sticky (#5478) 2026-01-15 10:25:37 +00:00
Balázs Szücs dbf72cf053 fix(auth): remove unnecessary authentication requirement from SignatureController (#5477)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>
2026-01-14 21:30:16 +00:00
stirlingbot[bot] 84e23abddc 🌐 Sync Translations + Update README Progress Table (#5473)
### Description of Changes

This Pull Request was automatically generated to synchronize updates to
translation files and documentation. Below are the details of the
changes made:

#### **1. Synchronization of Translation Files**
- Updated translation files
(`frontend/public/locales/*/translation.toml`) to reflect changes in the
reference file `en-GB/translation.toml`.
- Ensured consistency and synchronization across all supported language
files.
- Highlighted any missing or incomplete translations.
- **Format**: TOML

#### **2. Update README.md**
- Generated the translation progress table in `README.md` using
`counter_translation_v3.py`.
- Added a summary of the current translation status for all supported
languages.
- Included up-to-date statistics on translation coverage.

#### **Why these changes are necessary**
- Keeps translation files aligned with the latest reference updates.
- Ensures the documentation reflects the current translation progress.

---

Auto-generated by [create-pull-request][1].

[1]: https://github.com/peter-evans/create-pull-request

---------

Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com>
2026-01-14 12:12:43 +00:00
Anthony Stirling a7945da3b4 translations (#5469)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2026-01-14 11:26:45 +00:00
504 changed files with 42335 additions and 31670 deletions
-3
View File
@@ -63,9 +63,6 @@ labels:
files:
- 'app/core/src/main/resources/static/.*'
- 'app/proprietary/src/main/resources/static/.*'
- 'app/core/src/main/java/stirling/software/SPDF/controller/web/.*'
- 'app/core/src/main/java/stirling/software/SPDF/UI/.*'
- 'app/proprietary/src/main/java/stirling/software/proprietary/security/controller/web/.*'
- 'frontend/**'
- 'frontend/.*'
- 'frontend/**/.*'
+2 -2
View File
@@ -59,8 +59,8 @@ def find_duplicate_keys(file_path, keys=None, prefix=""):
return duplicates
# Maximum size for TOML files (e.g., 570 KB)
MAX_FILE_SIZE = 570 * 1024
# Maximum size for TOML files (e.g., 1 MB)
MAX_FILE_SIZE = 1000 * 1024
def parse_toml_file(file_path):
+280 -329
View File
@@ -97,9 +97,9 @@ cffi==2.0.0 \
--hash=sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453 \
--hash=sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf
# via weasyprint
cfgv==3.4.0 \
--hash=sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9 \
--hash=sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560
cfgv==3.5.0 \
--hash=sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0 \
--hash=sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132
# via pre-commit
cssselect2==0.8.0 \
--hash=sha256:46fc70ebc41ced7a32cd42d58b1884d72ade23d21e5a4eaaf022401c13f0e76e \
@@ -109,259 +109,269 @@ distlib==0.4.0 \
--hash=sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16 \
--hash=sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d
# via virtualenv
filelock==3.20.0 \
--hash=sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2 \
--hash=sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4
filelock==3.20.3 \
--hash=sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1 \
--hash=sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1
# via virtualenv
fonttools==4.60.1 \
--hash=sha256:022beaea4b73a70295b688f817ddc24ed3e3418b5036ffcd5658141184ef0d0c \
--hash=sha256:026290e4ec76583881763fac284aca67365e0be9f13a7fb137257096114cb3bc \
--hash=sha256:0b0835ed15dd5b40d726bb61c846a688f5b4ce2208ec68779bc81860adb5851a \
--hash=sha256:0eae96373e4b7c9e45d099d7a523444e3554360927225c1cdae221a58a45b856 \
--hash=sha256:122e1a8ada290423c493491d002f622b1992b1ab0b488c68e31c413390dc7eb2 \
--hash=sha256:1410155d0e764a4615774e5c2c6fc516259fe3eca5882f034eb9bfdbee056259 \
--hash=sha256:145daa14bf24824b677b9357c5e44fd8895c2a8f53596e1b9ea3496081dc692c \
--hash=sha256:1525796c3ffe27bb6268ed2a1bb0dcf214d561dfaf04728abf01489eb5339dce \
--hash=sha256:154cb6ee417e417bf5f7c42fe25858c9140c26f647c7347c06f0cc2d47eff003 \
--hash=sha256:2299df884c11162617a66b7c316957d74a18e3758c0274762d2cc87df7bc0272 \
--hash=sha256:2409d5fb7b55fd70f715e6d34e7a6e4f7511b8ad29a49d6df225ee76da76dd77 \
--hash=sha256:268ecda8ca6cb5c4f044b1fb9b3b376e8cd1b361cef275082429dc4174907038 \
--hash=sha256:282dafa55f9659e8999110bd8ed422ebe1c8aecd0dc396550b038e6c9a08b8ea \
--hash=sha256:2ee06fc57512144d8b0445194c2da9f190f61ad51e230f14836286470c99f854 \
--hash=sha256:3630e86c484263eaac71d117085d509cbcf7b18f677906824e4bace598fb70d2 \
--hash=sha256:398447f3d8c0c786cbf1209711e79080a40761eb44b27cdafffb48f52bcec258 \
--hash=sha256:4ba4bd646e86de16160f0fb72e31c3b9b7d0721c3e5b26b9fa2fc931dfdb2652 \
--hash=sha256:5664fd1a9ea7f244487ac8f10340c4e37664675e8667d6fee420766e0fb3cf08 \
--hash=sha256:583b7f8e3c49486e4d489ad1deacfb8d5be54a8ef34d6df824f6a171f8511d99 \
--hash=sha256:596ecaca36367027d525b3b426d8a8208169d09edcf8c7506aceb3a38bfb55c7 \
--hash=sha256:5c1015318e4fec75dd4943ad5f6a206d9727adf97410d58b7e32ab644a807914 \
--hash=sha256:66929e2ea2810c6533a5184f938502cfdaea4bc3efb7130d8cc02e1c1b4108d6 \
--hash=sha256:6ec722ee589e89a89f5b7574f5c45604030aa6ae24cb2c751e2707193b466fed \
--hash=sha256:6f68576bb4bbf6060c7ab047b1574a1ebe5c50a17de62830079967b211059ebb \
--hash=sha256:7473a8ed9ed09aeaa191301244a5a9dbe46fe0bf54f9d6cd21d83044c3321217 \
--hash=sha256:7b0c6d57ab00dae9529f3faf187f2254ea0aa1e04215cf2f1a8ec277c96661bc \
--hash=sha256:7b4c32e232a71f63a5d00259ca3d88345ce2a43295bb049d21061f338124246f \
--hash=sha256:8177ec9676ea6e1793c8a084a90b65a9f778771998eb919d05db6d4b1c0b114c \
--hash=sha256:839565cbf14645952d933853e8ade66a463684ed6ed6c9345d0faf1f0e868877 \
--hash=sha256:875cb7764708b3132637f6c5fb385b16eeba0f7ac9fa45a69d35e09b47045801 \
--hash=sha256:8a44788d9d91df72d1a5eac49b31aeb887a5f4aab761b4cffc4196c74907ea85 \
--hash=sha256:8b4eb332f9501cb1cd3d4d099374a1e1306783ff95489a1026bde9eb02ccc34a \
--hash=sha256:906306ac7afe2156fcf0042173d6ebbb05416af70f6b370967b47f8f00103bbb \
--hash=sha256:992775c9fbe2cf794786fa0ffca7f09f564ba3499b8fe9f2f80bd7197db60383 \
--hash=sha256:996a4d1834524adbb423385d5a629b868ef9d774670856c63c9a0408a3063401 \
--hash=sha256:9a52f254ce051e196b8fe2af4634c2d2f02c981756c6464dc192f1b6050b4e28 \
--hash=sha256:9d0ced62b59e0430b3690dbc5373df1c2aa7585e9a8ce38eff87f0fd993c5b01 \
--hash=sha256:a140761c4ff63d0cb9256ac752f230460ee225ccef4ad8f68affc723c88e2036 \
--hash=sha256:a184b2ea57b13680ab6d5fbde99ccef152c95c06746cb7718c583abd8f945ccc \
--hash=sha256:a3db56f153bd4c5c2b619ab02c5db5192e222150ce5a1bc10f16164714bc39ac \
--hash=sha256:a46b2f450bc79e06ef3b6394f0c68660529ed51692606ad7f953fc2e448bc903 \
--hash=sha256:a884aef09d45ba1206712c7dbda5829562d3fea7726935d3289d343232ecb0d3 \
--hash=sha256:b2cf105cee600d2de04ca3cfa1f74f1127f8455b71dbad02b9da6ec266e116d6 \
--hash=sha256:b33a7884fabd72bdf5f910d0cf46be50dce86a0362a65cfc746a4168c67eb96c \
--hash=sha256:b42d86938e8dda1cd9a1a87a6d82f1818eaf933348429653559a458d027446da \
--hash=sha256:b6379e7546ba4ae4b18f8ae2b9bc5960936007a1c0e30b342f662577e8bc3299 \
--hash=sha256:c7420a2696a44650120cdd269a5d2e56a477e2bfa9d95e86229059beb1c19e15 \
--hash=sha256:c8651e0d4b3bdeda6602b85fdc2abbefc1b41e573ecb37b6779c4ca50753a199 \
--hash=sha256:d066ea419f719ed87bc2c99a4a4bfd77c2e5949cb724588b9dd58f3fd90b92bf \
--hash=sha256:e6c58beb17380f7c2ea181ea11e7db8c0ceb474c9dd45f48e71e2cb577d146a1 \
--hash=sha256:e852d9dda9f93ad3651ae1e3bb770eac544ec93c3807888798eccddf84596537 \
--hash=sha256:ec3681a0cb34c255d76dd9d865a55f260164adb9fa02628415cdc2d43ee2c05d \
--hash=sha256:ee0c0b3b35b34f782afc673d503167157094a16f442ace7c6c5e0ca80b08f50c \
--hash=sha256:eedacb5c5d22b7097482fa834bda0dafa3d914a4e829ec83cdea2a01f8c813c4 \
--hash=sha256:ef00af0439ebfee806b25f24c8f92109157ff3fac5731dc7867957812e87b8d9 \
--hash=sha256:f0e8817c7d1a0c2eedebf57ef9a9896f3ea23324769a9a2061a80fe8852705ed \
--hash=sha256:f3d5be054c461d6a2268831f04091dc82753176f6ea06dc6047a5e168265a987 \
--hash=sha256:f4b5c37a5f40e4d733d3bbaaef082149bee5a5ea3156a785ff64d949bd1353fa
fonttools==4.61.1 \
--hash=sha256:0de30bfe7745c0d1ffa2b0b7048fb7123ad0d71107e10ee090fa0b16b9452e87 \
--hash=sha256:10d88e55330e092940584774ee5e8a6971b01fc2f4d3466a1d6c158230880796 \
--hash=sha256:11f35ad7805edba3aac1a3710d104592df59f4b957e30108ae0ba6c10b11dd75 \
--hash=sha256:15acc09befd16a0fb8a8f62bc147e1a82817542d72184acca9ce6e0aeda9fa6d \
--hash=sha256:17d2bf5d541add43822bcf0c43d7d847b160c9bb01d15d5007d84e2217aaa371 \
--hash=sha256:2180f14c141d2f0f3da43f3a81bc8aa4684860f6b0e6f9e165a4831f24e6a23b \
--hash=sha256:21e7c8d76f62ab13c9472ccf74515ca5b9a761d1bde3265152a6dc58700d895b \
--hash=sha256:41a7170d042e8c0024703ed13b71893519a1a6d6e18e933e3ec7507a2c26a4b2 \
--hash=sha256:41ed4b5ec103bd306bb68f81dc166e77409e5209443e5773cb4ed837bcc9b0d3 \
--hash=sha256:497c31ce314219888c0e2fce5ad9178ca83fe5230b01a5006726cdf3ac9f24d9 \
--hash=sha256:4c1b526c8d3f615a7b1867f38a9410849c8f4aef078535742198e942fba0e9bd \
--hash=sha256:4d7092bb38c53bbc78e9255a59158b150bcdc115a1e3b3ce0b5f267dc35dd63c \
--hash=sha256:4f5686e1fe5fce75d82d93c47a438a25bf0d1319d2843a926f741140b2b16e0c \
--hash=sha256:58b0ee0ab5b1fc9921eccfe11d1435added19d6494dde14e323f25ad2bc30c56 \
--hash=sha256:5ce02f38a754f207f2f06557523cd39a06438ba3aafc0639c477ac409fc64e37 \
--hash=sha256:5fade934607a523614726119164ff621e8c30e8fa1ffffbbd358662056ba69f0 \
--hash=sha256:5fe9fd43882620017add5eabb781ebfbc6998ee49b35bd7f8f79af1f9f99a958 \
--hash=sha256:64102ca87e84261419c3747a0d20f396eb024bdbeb04c2bfb37e2891f5fadcb5 \
--hash=sha256:664c5a68ec406f6b1547946683008576ef8b38275608e1cee6c061828171c118 \
--hash=sha256:6675329885c44657f826ef01d9e4fb33b9158e9d93c537d84ad8399539bc6f69 \
--hash=sha256:75c1a6dfac6abd407634420c93864a1e274ebc1c7531346d9254c0d8f6ca00f9 \
--hash=sha256:75da8f28eff26defba42c52986de97b22106cb8f26515b7c22443ebc9c2d3261 \
--hash=sha256:77efb033d8d7ff233385f30c62c7c79271c8885d5c9657d967ede124671bbdfb \
--hash=sha256:78a7d3ab09dc47ac1a363a493e6112d8cabed7ba7caad5f54dbe2f08676d1b47 \
--hash=sha256:7c7db70d57e5e1089a274cbb2b1fd635c9a24de809a231b154965d415d6c6d24 \
--hash=sha256:8c56c488ab471628ff3bfa80964372fc13504ece601e0d97a78ee74126b2045c \
--hash=sha256:91669ccac46bbc1d09e9273546181919064e8df73488ea087dcac3e2968df9ba \
--hash=sha256:9b666a475a65f4e839d3d10473fad6d47e0a9db14a2f4a224029c5bfde58ad2c \
--hash=sha256:9cfef3ab326780c04d6646f68d4b4742aae222e8b8ea1d627c74e38afcbc9d91 \
--hash=sha256:a13fc8aeb24bad755eea8f7f9d409438eb94e82cf86b08fe77a03fbc8f6a96b1 \
--hash=sha256:a75c301f96db737e1c5ed5fd7d77d9c34466de16095a266509e13da09751bd19 \
--hash=sha256:a76d4cb80f41ba94a6691264be76435e5f72f2cb3cab0b092a6212855f71c2f6 \
--hash=sha256:aed04cabe26f30c1647ef0e8fbb207516fd40fe9472e9439695f5c6998e60ac5 \
--hash=sha256:b148b56f5de675ee16d45e769e69f87623a4944f7443850bf9a9376e628a89d2 \
--hash=sha256:b501c862d4901792adaec7c25b1ecc749e2662543f68bb194c42ba18d6eec98d \
--hash=sha256:b846a1fcf8beadeb9ea4f44ec5bdde393e2f1569e17d700bfc49cd69bde75881 \
--hash=sha256:b931ae8f62db78861b0ff1ac017851764602288575d65b8e8ff1963fed419063 \
--hash=sha256:c33ab3ca9d3ccd581d58e989d67554e42d8d4ded94ab3ade3508455fe70e65f7 \
--hash=sha256:c6604b735bb12fef8e0efd5578c9fb5d3d8532d5001ea13a19cddf295673ee09 \
--hash=sha256:d8db08051fc9e7d8bc622f2112511b8107d8f27cd89e2f64ec45e9825e8288da \
--hash=sha256:d9203500f7c63545b4ce3799319fe4d9feb1a1b89b28d3cb5abd11b9dd64147e \
--hash=sha256:dc492779501fa723b04d0ab1f5be046797fee17d27700476edc7ee9ae535a61e \
--hash=sha256:e6bcdf33aec38d16508ce61fd81838f24c83c90a1d1b8c68982857038673d6b8 \
--hash=sha256:e76ce097e3c57c4bcb67c5aa24a0ecdbd9f74ea9219997a707a4061fbe2707aa \
--hash=sha256:eff1ac3cc66c2ac7cda1e64b4e2f3ffef474b7335f92fc3833fc632d595fcee6 \
--hash=sha256:f3cb4a569029b9f291f88aafc927dd53683757e640081ca8c412781ea144565e \
--hash=sha256:f79b168428351d11e10c5aeb61a74e1851ec221081299f4cf56036a95431c43a \
--hash=sha256:fa646ecec9528bef693415c79a86e733c70a4965dd938e9a226b0fc64c9d2e6c \
--hash=sha256:fe2efccb324948a11dd09d22136fe2ac8a97d6c1347cf0b58a911dcd529f66b7 \
--hash=sha256:fff4f534200a04b4a36e7ae3cb74493afe807b517a09e99cb4faa89a34ed6ecd
# via weasyprint
identify==2.6.15 \
--hash=sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757 \
--hash=sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf
identify==2.6.16 \
--hash=sha256:391ee4d77741d994189522896270b787aed8670389bfd60f326d677d64a6dfb0 \
--hash=sha256:846857203b5511bbe94d5a352a48ef2359532bc8f6727b5544077a0dcfb24980
# via pre-commit
nodeenv==1.9.1 \
--hash=sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f \
--hash=sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9
nodeenv==1.10.0 \
--hash=sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827 \
--hash=sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb
# via pre-commit
numpy==2.2.6 \
--hash=sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff \
--hash=sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47 \
--hash=sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84 \
--hash=sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d \
--hash=sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6 \
--hash=sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f \
--hash=sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b \
--hash=sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49 \
--hash=sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163 \
--hash=sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571 \
--hash=sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42 \
--hash=sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff \
--hash=sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491 \
--hash=sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4 \
--hash=sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566 \
--hash=sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf \
--hash=sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40 \
--hash=sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd \
--hash=sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06 \
--hash=sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282 \
--hash=sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680 \
--hash=sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db \
--hash=sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3 \
--hash=sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90 \
--hash=sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1 \
--hash=sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289 \
--hash=sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab \
--hash=sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c \
--hash=sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d \
--hash=sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb \
--hash=sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d \
--hash=sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a \
--hash=sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf \
--hash=sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1 \
--hash=sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2 \
--hash=sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a \
--hash=sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543 \
--hash=sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00 \
--hash=sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c \
--hash=sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f \
--hash=sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd \
--hash=sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868 \
--hash=sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303 \
--hash=sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83 \
--hash=sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3 \
--hash=sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d \
--hash=sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87 \
--hash=sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa \
--hash=sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f \
--hash=sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae \
--hash=sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda \
--hash=sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915 \
--hash=sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249 \
--hash=sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de \
--hash=sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8
numpy==2.4.1 \
--hash=sha256:0093e85df2960d7e4049664b26afc58b03236e967fb942354deef3208857a04c \
--hash=sha256:09aa8a87e45b55a1c2c205d42e2808849ece5c484b2aab11fecabec3841cafba \
--hash=sha256:0cce2a669e3c8ba02ee563c7835f92c153cf02edff1ae05e1823f1dde21b16a5 \
--hash=sha256:0e6e8f9d9ecf95399982019c01223dc130542960a12edfa8edd1122dfa66a8a8 \
--hash=sha256:0f118ce6b972080ba0758c6087c3617b5ba243d806268623dc34216d69099ba0 \
--hash=sha256:178de8f87948163d98a4c9ab5bee4ce6519ca918926ec8df195af582de28544d \
--hash=sha256:18e14c4d09d55eef39a6ab5b08406e84bc6869c1e34eef45564804f90b7e0574 \
--hash=sha256:2023ef86243690c2791fd6353e5b4848eedaa88ca8a2d129f462049f6d484696 \
--hash=sha256:20d4649c773f66cc2fc36f663e091f57c3b7655f936a4c681b4250855d1da8f5 \
--hash=sha256:2302dc0224c1cbc49bb94f7064f3f923a971bfae45c33870dcbff63a2a550505 \
--hash=sha256:26f0bcd9c79a00e339565b303badc74d3ea2bd6d52191eeca5f95936cad107d0 \
--hash=sha256:297c72b1b98100c2e8f873d5d35fb551fce7040ade83d67dd51d38c8d42a2162 \
--hash=sha256:2f44de05659b67d20499cbc96d49f2650769afcb398b79b324bb6e297bfe3844 \
--hash=sha256:2ffd257026eb1b34352e749d7cc1678b5eeec3e329ad8c9965a797e08ccba205 \
--hash=sha256:382ad67d99ef49024f11d1ce5dcb5ad8432446e4246a4b014418ba3a1175a1f4 \
--hash=sha256:3869ea1ee1a1edc16c29bbe3a2f2a4e515cc3a44d43903ad41e0cacdbaf733dc \
--hash=sha256:3d1a100e48cb266090a031397863ff8a30050ceefd798f686ff92c67a486753d \
--hash=sha256:423797bdab2eeefbe608d7c1ec7b2b4fd3c58d51460f1ee26c7500a1d9c9ee93 \
--hash=sha256:42d7dd5fa36d16d52a84f821eb96031836fd405ee6955dd732f2023724d0aa01 \
--hash=sha256:49e792ec351315e16da54b543db06ca8a86985ab682602d90c60ef4ff4db2a9c \
--hash=sha256:4e53170557d37ae404bf8d542ca5b7c629d6efa1117dac6a83e394142ea0a43f \
--hash=sha256:4f1b68ff47680c2925f8063402a693ede215f0257f02596b1318ecdfb1d79e33 \
--hash=sha256:4f9c360ecef085e5841c539a9a12b883dff005fbd7ce46722f5e9cef52634d82 \
--hash=sha256:529050522e983e00a6c1c6b67411083630de8b57f65e853d7b03d9281b8694d2 \
--hash=sha256:52b5f61bdb323b566b528899cc7db2ba5d1015bda7ea811a8bcf3c89c331fa42 \
--hash=sha256:538bf4ec353709c765ff75ae616c34d3c3dca1a68312727e8f2676ea644f8509 \
--hash=sha256:5adf01965456a664fc727ed69cc71848f28d063217c63e1a0e200a118d5eec9a \
--hash=sha256:5b55aa56165b17aaf15520beb9cbd33c9039810e0d9643dd4379e44294c7303e \
--hash=sha256:5d558123217a83b2d1ba316b986e9248a1ed1971ad495963d555ccd75dcb1556 \
--hash=sha256:5de60946f14ebe15e713a6f22850c2372fa72f4ff9a432ab44aa90edcadaa65a \
--hash=sha256:62fea415f83ad8fdb6c20840578e5fbaf5ddd65e0ec6c3c47eda0f69da172510 \
--hash=sha256:6436cffb4f2bf26c974344439439c95e152c9a527013f26b3577be6c2ca64295 \
--hash=sha256:6461de5113088b399d655d45c3897fa188766415d0f568f175ab071c8873bd73 \
--hash=sha256:69e7419c9012c4aaf695109564e3387f1259f001b4326dfa55907b098af082d3 \
--hash=sha256:71abbea030f2cfc3092a0ff9f8c8fdefdc5e0bf7d9d9c99663538bb0ecdac0b9 \
--hash=sha256:7211b95ca365519d3596a1d8688a95874cc94219d417504d9ecb2df99fa7bfa8 \
--hash=sha256:727c6c3275ddefa0dc078524a85e064c057b4f4e71ca5ca29a19163c607be745 \
--hash=sha256:79e9e06c4c2379db47f3f6fc7a8652e7498251789bf8ff5bd43bf478ef314ca2 \
--hash=sha256:7ad270f438cbdd402c364980317fb6b117d9ec5e226fff5b4148dd9aa9fc6e02 \
--hash=sha256:7d5d7999df434a038d75a748275cd6c0094b0ecdb0837342b332a82defc4dc4d \
--hash=sha256:8097529164c0f3e32bb89412a0905d9100bf434d9692d9fc275e18dcf53c9344 \
--hash=sha256:82c55962006156aeef1629b953fd359064aa47e4d82cfc8e67f0918f7da3344f \
--hash=sha256:8361ea4220d763e54cff2fbe7d8c93526b744f7cd9ddab47afeff7e14e8503be \
--hash=sha256:899d2c18024984814ac7e83f8f49d8e8180e2fbe1b2e252f2e7f1d06bea92425 \
--hash=sha256:8ad35f20be147a204e28b6a0575fbf3540c5e5f802634d4258d55b1ff5facce1 \
--hash=sha256:8f085da926c0d491ffff3096f91078cc97ea67e7e6b65e490bc8dcda65663be2 \
--hash=sha256:9171a42fcad32dcf3fa86f0a4faa5e9f8facefdb276f54b8b390d90447cff4e2 \
--hash=sha256:92a0e65272fd60bfa0d9278e0484c2f52fe03b97aedc02b357f33fe752c52ffb \
--hash=sha256:941c2a93313d030f219f3a71fd3d91a728b82979a5e8034eb2e60d394a2b83f9 \
--hash=sha256:98b35775e03ab7f868908b524fc0a84d38932d8daf7b7e1c3c3a1b6c7a2c9f15 \
--hash=sha256:a1ceafc5042451a858231588a104093474c6a5c57dcc724841f5c888d237d690 \
--hash=sha256:a73044b752f5d34d4232f25f18160a1cc418ea4507f5f11e299d8ac36875f8a0 \
--hash=sha256:a7870e8c5fc11aef57d6fea4b4085e537a3a60ad2cdd14322ed531fdca68d261 \
--hash=sha256:a92f227dbcdc9e4c3e193add1a189a9909947d4f8504c576f4a732fd0b54240a \
--hash=sha256:ac08c63cb7779b85e9d5318e6c3518b424bc1f364ac4cb2c6136f12e5ff2dccc \
--hash=sha256:b6bcf39112e956594b3331316d90c90c90fb961e39696bda97b89462f5f3943f \
--hash=sha256:c0faba4a331195bfa96f93dd9dfaa10b2c7aa8cda3a02b7fd635e588fe821bf5 \
--hash=sha256:ce9ce141a505053b3c7bce3216071f3bf5c182b8b28930f14cd24d43932cd2df \
--hash=sha256:cf6470d91d34bf669f61d515499859fa7a4c2f7c36434afb70e82df7217933f9 \
--hash=sha256:d3703409aac693fa82c0aee023a1ae06a6e9d065dba10f5e8e80f642f1e9d0a2 \
--hash=sha256:d3e3087f53e2b4428766b54932644d148613c5a595150533ae7f00dab2f319a8 \
--hash=sha256:d3f8f0df9f4b8be57b3bf74a1d087fec68f927a2fab68231fdb442bf2c12e426 \
--hash=sha256:d797454e37570cfd61143b73b8debd623c3c0952959adb817dd310a483d58a1b \
--hash=sha256:e1a27bb1b2dee45a2a53f5ca6ff2d1a7f135287883a1689e930d44d1ff296c87 \
--hash=sha256:e3bd2cb07841166420d2fa7146c96ce00cb3410664cbc1a6be028e456c4ee220 \
--hash=sha256:e7b6b5e28bbd47b7532698e5db2fe1db693d84b58c254e4389d99a27bb9b8f6b \
--hash=sha256:e867df947d427cdd7a60e3e271729090b0f0df80f5f10ab7dd436f40811699c3 \
--hash=sha256:ea66d2b41ca4a1630aae5507ee0a71647d3124d1741980138aa8f28f44dac36e \
--hash=sha256:edee228f76ee2dab4579fad6f51f6a305de09d444280109e0f75df247ff21501 \
--hash=sha256:f0a90aba7d521e6954670550e561a4cb925713bd944445dbe9e729b71f6cabee \
--hash=sha256:f93bc6892fe7b0663e5ffa83b61aab510aacffd58c16e012bb9352d489d90cb7 \
--hash=sha256:fb1461c99de4d040666ca0444057b06541e5642f800b71c56e6ea92d6a853a0c
# via opencv-python-headless
opencv-python-headless==4.12.0.88 \
--hash=sha256:1e58d664809b3350c1123484dd441e1667cd7bed3086db1b9ea1b6f6cb20b50e \
--hash=sha256:236c8df54a90f4d02076e6f9c1cc763d794542e886c576a6fee46ec8ff75a7a9 \
--hash=sha256:365bb2e486b50feffc2d07a405b953a8f3e8eaa63865bc650034e5c71e7a5154 \
--hash=sha256:86b413bdd6c6bf497832e346cd5371995de148e579b9774f8eba686dee3f5528 \
--hash=sha256:aeb4b13ecb8b4a0beb2668ea07928160ea7c2cd2d9b5ef571bbee6bafe9cc8d0 \
--hash=sha256:cfdc017ddf2e59b6c2f53bc12d74b6b0be7ded4ec59083ea70763921af2b6c09 \
--hash=sha256:fde2cf5c51e4def5f2132d78e0c08f9c14783cd67356922182c6845b9af87dbd
opencv-python-headless==4.13.0.90 \
--hash=sha256:0e0c8c9f620802fddc4fa7f471a1d263c7b0dca16cd9e7e2f996bb8bd2128c0c \
--hash=sha256:12a28674f215542c9bf93338de1b5bffd76996d32da9acb9e739fdb9c8bbd738 \
--hash=sha256:32255203040dc98803be96362e13f9e4bce20146898222d2e5c242f80de50da5 \
--hash=sha256:96060fc57a1abb1144b0b8129e2ff3bfcdd0ccd8e8bd05bd85256ff4ed587d3b \
--hash=sha256:dbc1f4625e5af3a80ebdbd84380227c0f445228588f2521b11af47710caca1ba \
--hash=sha256:e13790342591557050157713af17a7435ac1b50c65282715093c9297fa045d8f \
--hash=sha256:eba38bc255d0b7d1969c5bcc90a060ca2b61a3403b613872c750bfa5dfe9e03b \
--hash=sha256:f46b17ea0aa7e4124ca6ad71143f89233ae9557f61d2326bcdb34329a1ddf9bd
# via -r .github/scripts/requirements_dev.in
pdf2image==1.17.0 \
--hash=sha256:eaa959bc116b420dd7ec415fcae49b98100dda3dd18cd2fdfa86d09f112f6d57 \
--hash=sha256:ecdd58d7afb810dffe21ef2b1bbc057ef434dabbac6c33778a38a3f7744a27e2
# via -r .github/scripts/requirements_dev.in
pillow==12.0.0 \
--hash=sha256:0869154a2d0546545cde61d1789a6524319fc1897d9ee31218eae7a60ccc5643 \
--hash=sha256:09f2d0abef9e4e2f349305a4f8cc784a8a6c2f58a8c4892eea13b10a943bd26e \
--hash=sha256:0b817e7035ea7f6b942c13aa03bb554fc44fea70838ea21f8eb31c638326584e \
--hash=sha256:0fd00cac9c03256c8b2ff58f162ebcd2587ad3e1f2e397eab718c47e24d231cc \
--hash=sha256:110486b79f2d112cf6add83b28b627e369219388f64ef2f960fef9ebaf54c642 \
--hash=sha256:1979f4566bb96c1e50a62d9831e2ea2d1211761e5662afc545fa766f996632f6 \
--hash=sha256:1ac11e8ea4f611c3c0147424eae514028b5e9077dd99ab91e1bd7bc33ff145e1 \
--hash=sha256:1b1b133e6e16105f524a8dec491e0586d072948ce15c9b914e41cdadd209052b \
--hash=sha256:1ee80a59f6ce048ae13cda1abf7fbd2a34ab9ee7d401c46be3ca685d1999a399 \
--hash=sha256:21f241bdd5080a15bc86d3466a9f6074a9c2c2b314100dd896ac81ee6db2f1ba \
--hash=sha256:266cd5f2b63ff316d5a1bba46268e603c9caf5606d44f38c2873c380950576ad \
--hash=sha256:26d9f7d2b604cd23aba3e9faf795787456ac25634d82cd060556998e39c6fa47 \
--hash=sha256:27f95b12453d165099c84f8a8bfdfd46b9e4bda9e0e4b65f0635430027f55739 \
--hash=sha256:2c54c1a783d6d60595d3514f0efe9b37c8808746a66920315bfd34a938d7994b \
--hash=sha256:2fa5f0b6716fc88f11380b88b31fe591a06c6315e955c096c35715788b339e3f \
--hash=sha256:32ed80ea8a90ee3e6fa08c21e2e091bba6eda8eccc83dbc34c95169507a91f10 \
--hash=sha256:3830c769decf88f1289680a59d4f4c46c72573446352e2befec9a8512104fa52 \
--hash=sha256:38df9b4bfd3db902c9c2bd369bcacaf9d935b2fff73709429d95cc41554f7b3d \
--hash=sha256:3adfb466bbc544b926d50fe8f4a4e6abd8c6bffd28a26177594e6e9b2b76572b \
--hash=sha256:3e42edad50b6909089750e65c91aa09aaf1e0a71310d383f11321b27c224ed8a \
--hash=sha256:4078242472387600b2ce8d93ade8899c12bf33fa89e55ec89fe126e9d6d5d9e9 \
--hash=sha256:455247ac8a4cfb7b9bc45b7e432d10421aea9fc2e74d285ba4072688a74c2e9d \
--hash=sha256:4cc6b3b2efff105c6a1656cfe59da4fdde2cda9af1c5e0b58529b24525d0a098 \
--hash=sha256:4cf7fed4b4580601c4345ceb5d4cbf5a980d030fd5ad07c4d2ec589f95f09905 \
--hash=sha256:5193fde9a5f23c331ea26d0cf171fbf67e3f247585f50c08b3e205c7aeb4589b \
--hash=sha256:5269cc1caeedb67e6f7269a42014f381f45e2e7cd42d834ede3c703a1d915fe3 \
--hash=sha256:53561a4ddc36facb432fae7a9d8afbfaf94795414f5cdc5fc52f28c1dca90371 \
--hash=sha256:55f818bd74fe2f11d4d7cbc65880a843c4075e0ac7226bc1a23261dbea531953 \
--hash=sha256:58eea5ebe51504057dd95c5b77d21700b77615ab0243d8152793dc00eb4faf01 \
--hash=sha256:5d5c411a8eaa2299322b647cd932586b1427367fd3184ffbb8f7a219ea2041ca \
--hash=sha256:6846bd2d116ff42cba6b646edf5bf61d37e5cbd256425fa089fee4ff5c07a99e \
--hash=sha256:6ace95230bfb7cd79ef66caa064bbe2f2a1e63d93471c3a2e1f1348d9f22d6b7 \
--hash=sha256:6e51b71417049ad6ab14c49608b4a24d8fb3fe605e5dfabfe523b58064dc3d27 \
--hash=sha256:71db6b4c1653045dacc1585c1b0d184004f0d7e694c7b34ac165ca70c0838082 \
--hash=sha256:7438839e9e053ef79f7112c881cef684013855016f928b168b81ed5835f3e75e \
--hash=sha256:759de84a33be3b178a64c8ba28ad5c135900359e85fb662bc6e403ad4407791d \
--hash=sha256:792a2c0be4dcc18af9d4a2dfd8a11a17d5e25274a1062b0ec1c2d79c76f3e7f8 \
--hash=sha256:7d87ef5795da03d742bf49439f9ca4d027cde49c82c5371ba52464aee266699a \
--hash=sha256:7dfb439562f234f7d57b1ac6bc8fe7f838a4bd49c79230e0f6a1da93e82f1fad \
--hash=sha256:7fa22993bac7b77b78cae22bad1e2a987ddf0d9015c63358032f84a53f23cdc3 \
--hash=sha256:805ebf596939e48dbb2e4922a1d3852cfc25c38160751ce02da93058b48d252a \
--hash=sha256:82240051c6ca513c616f7f9da06e871f61bfd7805f566275841af15015b8f98d \
--hash=sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353 \
--hash=sha256:8d8ca2b210ada074d57fcee40c30446c9562e542fc46aedc19baf758a93532ee \
--hash=sha256:8dc232e39d409036af549c86f24aed8273a40ffa459981146829a324e0848b4b \
--hash=sha256:90387104ee8400a7b4598253b4c406f8958f59fcf983a6cea2b50d59f7d63d0b \
--hash=sha256:905b0365b210c73afb0ebe9101a32572152dfd1c144c7e28968a331b9217b94a \
--hash=sha256:99353a06902c2e43b43e8ff74ee65a7d90307d82370604746738a1e0661ccca7 \
--hash=sha256:99a7f72fb6249302aa62245680754862a44179b545ded638cf1fef59befb57ef \
--hash=sha256:9f0b04c6b8584c2c193babcccc908b38ed29524b29dd464bc8801bf10d746a3a \
--hash=sha256:9fe611163f6303d1619bbcb653540a4d60f9e55e622d60a3108be0d5b441017a \
--hash=sha256:a3475b96f5908b3b16c47533daaa87380c491357d197564e0ba34ae75c0f3257 \
--hash=sha256:a6597ff2b61d121172f5844b53f21467f7082f5fb385a9a29c01414463f93b07 \
--hash=sha256:a7921c5a6d31b3d756ec980f2f47c0cfdbce0fc48c22a39347a895f41f4a6ea4 \
--hash=sha256:aa5129de4e174daccbc59d0a3b6d20eaf24417d59851c07ebb37aeb02947987c \
--hash=sha256:aeaefa96c768fc66818730b952a862235d68825c178f1b3ffd4efd7ad2edcb7c \
--hash=sha256:afbefa430092f71a9593a99ab6a4e7538bc9eabbf7bf94f91510d3503943edc4 \
--hash=sha256:aff9e4d82d082ff9513bdd6acd4f5bd359f5b2c870907d2b0a9c5e10d40c88fe \
--hash=sha256:b22bd8c974942477156be55a768f7aa37c46904c175be4e158b6a86e3a6b7ca8 \
--hash=sha256:b290fd8aa38422444d4b50d579de197557f182ef1068b75f5aa8558638b8d0a5 \
--hash=sha256:b2e4b27a6e15b04832fe9bf292b94b5ca156016bbc1ea9c2c20098a0320d6cf6 \
--hash=sha256:b583dc9070312190192631373c6c8ed277254aa6e6084b74bdd0a6d3b221608e \
--hash=sha256:b87843e225e74576437fd5b6a4c2205d422754f84a06942cfaf1dc32243e45a8 \
--hash=sha256:bc91a56697869546d1b8f0a3ff35224557ae7f881050e99f615e0119bf934b4e \
--hash=sha256:bd87e140e45399c818fac4247880b9ce719e4783d767e030a883a970be632275 \
--hash=sha256:bde737cff1a975b70652b62d626f7785e0480918dece11e8fef3c0cf057351c3 \
--hash=sha256:bdee52571a343d721fb2eb3b090a82d959ff37fc631e3f70422e0c2e029f3e76 \
--hash=sha256:bee2a6db3a7242ea309aa7ee8e2780726fed67ff4e5b40169f2c940e7eb09227 \
--hash=sha256:beeae3f27f62308f1ddbcfb0690bf44b10732f2ef43758f169d5e9303165d3f9 \
--hash=sha256:c50f36a62a22d350c96e49ad02d0da41dbd17ddc2e29750dbdba4323f85eb4a5 \
--hash=sha256:c607c90ba67533e1b2355b821fef6764d1dd2cbe26b8c1005ae84f7aea25ff79 \
--hash=sha256:c7b2a63fd6d5246349f3d3f37b14430d73ee7e8173154461785e43036ffa96ca \
--hash=sha256:c828a1ae702fc712978bda0320ba1b9893d99be0badf2647f693cc01cf0f04fa \
--hash=sha256:c85de1136429c524e55cfa4e033b4a7940ac5c8ee4d9401cc2d1bf48154bbc7b \
--hash=sha256:c98fa880d695de164b4135a52fd2e9cd7b7c90a9d8ac5e9e443a24a95ef9248e \
--hash=sha256:cae81479f77420d217def5f54b5b9d279804d17e982e0f2fa19b1d1e14ab5197 \
--hash=sha256:d034140032870024e6b9892c692fe2968493790dd57208b2c37e3fb35f6df3ab \
--hash=sha256:d120c38a42c234dc9a8c5de7ceaaf899cf33561956acb4941653f8bdc657aa79 \
--hash=sha256:d4827615da15cd59784ce39d3388275ec093ae3ee8d7f0c089b76fa87af756c2 \
--hash=sha256:d49e2314c373f4c2b39446fb1a45ed333c850e09d0c59ac79b72eb3b95397363 \
--hash=sha256:d52610d51e265a51518692045e372a4c363056130d922a7351429ac9f27e70b0 \
--hash=sha256:d64317d2587c70324b79861babb9c09f71fbb780bad212018874b2c013d8600e \
--hash=sha256:d77153e14b709fd8b8af6f66a3afbb9ed6e9fc5ccf0b6b7e1ced7b036a228782 \
--hash=sha256:d7e091d464ac59d2c7ad8e7e08105eaf9dafbc3883fd7265ffccc2baad6ac925 \
--hash=sha256:dd333073e0cacdc3089525c7df7d39b211bcdf31fc2824e49d01c6b6187b07d0 \
--hash=sha256:e5d8efac84c9afcb40914ab49ba063d94f5dbdf5066db4482c66a992f47a3a3b \
--hash=sha256:f135c702ac42262573fe9714dfe99c944b4ba307af5eb507abef1667e2cbbced \
--hash=sha256:f13711b1a5ba512d647a0e4ba79280d3a9a045aaf7e0cc6fbe96b91d4cdf6b0c \
--hash=sha256:f4f1231b7dec408e8670264ce63e9c71409d9583dd21d32c163e25213ee2a344 \
--hash=sha256:fa3ed2a29a9e9d2d488b4da81dcb54720ac3104a20bf0bd273f1e4648aff5af9 \
--hash=sha256:fb3096c30df99fd01c7bf8e544f392103d0795b9f98ba71a8054bcbf56b255f1
pillow==12.1.0 \
--hash=sha256:00162e9ca6d22b7c3ee8e61faa3c3253cd19b6a37f126cad04f2f88b306f557d \
--hash=sha256:079af2fb0c599c2ec144ba2c02766d1b55498e373b3ac64687e43849fbbef5bc \
--hash=sha256:0b022eaaf709541b391ee069f0022ee5b36c709df71986e3f7be312e46f42c84 \
--hash=sha256:0c27407a2d1b96774cbc4a7594129cc027339fd800cd081e44497722ea1179de \
--hash=sha256:0ddedfaa8b5f0b4ffbc2fa87b556dc59f6bb4ecb14a53b33f9189713ae8053c0 \
--hash=sha256:0deedf2ea233722476b3a81e8cdfbad786f7adbed5d848469fa59fe52396e4ef \
--hash=sha256:0ed07dca4a8464bada6139ab38f5382f83e5f111698caf3191cb8dbf27d908b4 \
--hash=sha256:0fde7ec5538ab5095cc02df38ee99b0443ff0e1c847a045554cf5f9af1f4aa82 \
--hash=sha256:15c794d74303828eaa957ff8070846d0efe8c630901a1c753fdc63850e19ecd9 \
--hash=sha256:1a949604f73eb07a8adab38c4fe50791f9919344398bdc8ac6b307f755fc7030 \
--hash=sha256:1f345e7bc9d7f368887c712aa5054558bad44d2a301ddf9248599f4161abc7c0 \
--hash=sha256:1fcc52d86ce7a34fd17cb04e87cfdb164648a3662a6f20565910a99653d66c18 \
--hash=sha256:21e686a21078b0f9cb8c8a961d99e6a4ddb88e0fc5ea6e130172ddddc2e5221a \
--hash=sha256:2415373395a831f53933c23ce051021e79c8cd7979822d8cc478547a3f4da8ef \
--hash=sha256:277518bf4fe74aa91489e1b20577473b19ee70fb97c374aa50830b279f25841b \
--hash=sha256:27b9baecb428899db6c0de572d6d305cfaf38ca1596b5c0542a5182e3e74e8c6 \
--hash=sha256:29a4cef9cb672363926f0470afc516dbf7305a14d8c54f7abbb5c199cd8f8179 \
--hash=sha256:3413c2ae377550f5487991d444428f1a8ae92784aac79caa8b1e3b89b175f77e \
--hash=sha256:351889afef0f485b84078ea40fe33727a0492b9af3904661b0abbafee0355b72 \
--hash=sha256:3ffaa2f0659e2f740473bcf03c702c39a8d4b2b7ffc629052028764324842c64 \
--hash=sha256:40a8e3b9e8773876d6e30daed22f016509e3987bab61b3b7fe309d7019a87451 \
--hash=sha256:414b9a78e14ffeb98128863314e62c3f24b8a86081066625700b7985b3f529bd \
--hash=sha256:43aca0a55ce1eefc0aefa6253661cb54571857b1a7b2964bd8a1e3ef4b729924 \
--hash=sha256:43b4899cfd091a9693a1278c4982f3e50f7fb7cff5153b05174b4afc9593b616 \
--hash=sha256:461f9dfdafa394c59cd6d818bdfdbab4028b83b02caadaff0ffd433faf4c9a7a \
--hash=sha256:4f9f6a650743f0ddee5593ac9e954ba1bdbc5e150bc066586d4f26127853ab94 \
--hash=sha256:53d8b764726d3af1a138dd353116f774e3862ec7e3794e0c8781e30db0f35dfc \
--hash=sha256:565c986f4b45c020f5421a4cea13ef294dde9509a8577f29b2fc5edc7587fff8 \
--hash=sha256:5c5ae0a06e9ea030ab786b0251b32c7e4ce10e58d983c0d5c56029455180b5b9 \
--hash=sha256:5cb7bc1966d031aec37ddb9dcf15c2da5b2e9f7cc3ca7c54473a20a927e1eb91 \
--hash=sha256:5da841d81b1a05ef940a8567da92decaa15bc4d7dedb540a8c219ad83d91808a \
--hash=sha256:5fee4c04aad8932da9f8f710af2c1a15a83582cfb884152a9caa79d4efcdbf9c \
--hash=sha256:609e89d9f90b581c8d16358c9087df76024cf058fa693dd3e1e1620823f39670 \
--hash=sha256:6258f3260986990ba2fa8a874f8b6e808cf5abb51a94015ca3dc3c68aa4f30ea \
--hash=sha256:64efdf00c09e31efd754448a383ea241f55a994fd079866b92d2bbff598aad91 \
--hash=sha256:65b80c1ee7e14a87d6a068dd3b0aea268ffcabfe0498d38661b00c5b4b22e74c \
--hash=sha256:6741e6f3074a35e47c77b23a4e4f2d90db3ed905cb1c5e6e0d49bff2045632bc \
--hash=sha256:681088909d7e8fa9e31b9799aaa59ba5234c58e5e4f1951b4c4d1082a2e980e0 \
--hash=sha256:6b7a9d1db5dad90e2991645874f708e87d9a3c370c243c2d7684d28f7e133e6b \
--hash=sha256:7315f9137087c4e0ee73a761b163fc9aa3b19f5f606a7fc08d83fd3e4379af65 \
--hash=sha256:742aea052cf5ab5034a53c3846165bc3ce88d7c38e954120db0ab867ca242661 \
--hash=sha256:75af0b4c229ac519b155028fa1be632d812a519abba9b46b20e50c6caa184f19 \
--hash=sha256:7b5dd7cbae20285cdb597b10eb5a2c13aa9de6cde9bb64a3c1317427b1db1ae1 \
--hash=sha256:7d6daa89a00b58c37cb1747ec9fb7ac3bc5ffd5949f5888657dfddde6d1312e0 \
--hash=sha256:800429ac32c9b72909c671aaf17ecd13110f823ddb7db4dfef412a5587c2c24e \
--hash=sha256:806f3987ffe10e867bab0ddad45df1148a2b98221798457fa097ad85d6e8bc75 \
--hash=sha256:808b99604f7873c800c4840f55ff389936ef1948e4e87645eaf3fccbc8477ac4 \
--hash=sha256:80941e6d573197a0c28f394753de529bb436b1ca990ed6e765cf42426abc39f8 \
--hash=sha256:84cabc7095dd535ca934d57e9ce2a72ffd216e435a84acb06b2277b1de2689bd \
--hash=sha256:8637e29d13f478bc4f153d8daa9ffb16455f0a6cb287da1b432fdad2bfbd66c7 \
--hash=sha256:896866d2d436563fa2a43a9d72f417874f16b5545955c54a64941e87c1376c61 \
--hash=sha256:8e178e3e99d3c0ea8fc64b88447f7cac8ccf058af422a6cedc690d0eadd98c51 \
--hash=sha256:907bfa8a9cb790748a9aa4513e37c88c59660da3bcfffbd24a7d9e6abf224551 \
--hash=sha256:9212d6b86917a2300669511ed094a9406888362e085f2431a7da985a6b124f45 \
--hash=sha256:92a7fe4225365c5e3a8e598982269c6d6698d3e783b3b1ae979e7819f9cd55c1 \
--hash=sha256:935b9d1aed48fcfb3f838caac506f38e29621b44ccc4f8a64d575cb1b2a88644 \
--hash=sha256:97e9993d5ed946aba26baf9c1e8cf18adbab584b99f452ee72f7ee8acb882796 \
--hash=sha256:983976c2ab753166dc66d36af6e8ec15bb511e4a25856e2227e5f7e00a160587 \
--hash=sha256:9f5fefaca968e700ad1a4a9de98bf0869a94e397fe3524c4c9450c1445252304 \
--hash=sha256:a332ac4ccb84b6dde65dbace8431f3af08874bf9770719d32a635c4ef411b18b \
--hash=sha256:a40905599d8079e09f25027423aed94f2823adaf2868940de991e53a449e14a8 \
--hash=sha256:a6dfc2af5b082b635af6e08e0d1f9f1c4e04d17d4e2ca0ef96131e85eda6eb17 \
--hash=sha256:a786bf667724d84aa29b5db1c61b7bfdde380202aaca12c3461afd6b71743171 \
--hash=sha256:a83e0850cb8f5ac975291ebfc4170ba481f41a28065277f7f735c202cd8e0af3 \
--hash=sha256:aa0c9cc0b82b14766a99fbe6084409972266e82f459821cd26997a488a7261a7 \
--hash=sha256:b17fbdbe01c196e7e159aacb889e091f28e61020a8abeac07b68079b6e626988 \
--hash=sha256:b63e13dd27da389ed9475b3d28510f0f954bca0041e8e551b2a4eb1eab56a39a \
--hash=sha256:b6e53e82ec2db0717eabb276aa56cf4e500c9a7cec2c2e189b55c24f65a3e8c0 \
--hash=sha256:bb0984b30e973f7e2884362b7d23d0a348c7143ee559f38ef3eaab640144204c \
--hash=sha256:bc11908616c8a283cf7d664f77411a5ed2a02009b0097ff8abbba5e79128ccf2 \
--hash=sha256:bdec5e43377761c5dbca620efb69a77f6855c5a379e32ac5b158f54c84212b14 \
--hash=sha256:bef9768cab184e7ae6e559c032e95ba8d07b3023c289f79a2bd36e8bf85605a5 \
--hash=sha256:c990547452ee2800d8506c4150280757f88532f3de2a58e3022e9b179107862a \
--hash=sha256:ca94b6aac0d7af2a10ba08c0f888b3d5114439b6b3ef39968378723622fed377 \
--hash=sha256:cad302dc10fac357d3467a74a9561c90609768a6f73a1923b0fd851b6486f8b0 \
--hash=sha256:d0a7735df32ccbcc98b98a1ac785cc4b19b580be1bdf0aeb5c03223220ea09d5 \
--hash=sha256:d70347c8a5b7ccd803ec0c85c8709f036e6348f1e6a5bf048ecd9c64d3550b8b \
--hash=sha256:d70534cea9e7966169ad29a903b99fc507e932069a881d0965a1a84bb57f6c6d \
--hash=sha256:db44d5c160a90df2d24a24760bbd37607d53da0b34fb546c4c232af7192298ac \
--hash=sha256:e115c15e3bc727b1ca3e641a909f77f8ca72a64fff150f666fcc85e57701c26c \
--hash=sha256:e2479c7f02f9d505682dc47df8c0ea1fc5e264c4d1629a5d63fe3e2334b89554 \
--hash=sha256:e5dcbe95016e88437ecf33544ba5db21ef1b8dd6e1b434a2cb2a3d605299e643 \
--hash=sha256:e6bdb408f7c9dd2a5ff2b14a3b0bb6d4deb29fb9961e6eb3ae2031ae9a5cec13 \
--hash=sha256:e75d3dba8fc1ddfec0cd752108f93b83b4f8d6ab40e524a95d35f016b9683b09 \
--hash=sha256:efdc140e7b63b8f739d09a99033aa430accce485ff78e6d311973a67b6bf3208 \
--hash=sha256:f10c98f49227ed8383d28174ee95155a675c4ed7f85e2e573b04414f7e371bda \
--hash=sha256:f188028b5af6b8fb2e9a76ac0f841a575bd1bd396e46ef0840d9b88a48fdbcea \
--hash=sha256:f188d580bd870cda1e15183790d1cc2fa78f666e76077d103edf048eed9c356e \
--hash=sha256:f45bd71d1fa5e5749587613037b172e0b3b23159d1c00ef2fc920da6f470e6f0 \
--hash=sha256:f61333d817698bdcdd0f9d7793e365ac3d2a21c1f1eb02b32ad6aefb8d8ea831 \
--hash=sha256:fb125d860738a09d363a88daa0f59c4533529a90e564785e20fe875b200b6dbd
# via
# -r .github/scripts/requirements_dev.in
# pdf2image
# weasyprint
platformdirs==4.5.0 \
--hash=sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312 \
--hash=sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3
platformdirs==4.5.1 \
--hash=sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda \
--hash=sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31
# via virtualenv
pre-commit==4.3.0 \
--hash=sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8 \
--hash=sha256:499fe450cc9d42e9d58e606262795ecb64dd05438943c62b66f6a8673da30b16
pre-commit==4.5.1 \
--hash=sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77 \
--hash=sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61
# via -r .github/scripts/requirements_dev.in
pycparser==2.23 \
--hash=sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2 \
--hash=sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934
pycparser==3.0 \
--hash=sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29 \
--hash=sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992
# via cffi
pydyf==0.11.0 \
--hash=sha256:0aaf9e2ebbe786ec7a78ec3fbffa4cdcecde53fd6f563221d53c6bc1328848a3 \
--hash=sha256:394dddf619cca9d0c55715e3c55ea121a9bf9cbc780cdc1201a2427917b86b64
pydyf==0.12.1 \
--hash=sha256:ea25b4e1fe7911195cb57067560daaa266639184e8335365cc3ee5214e7eaadc \
--hash=sha256:fbd7e759541ac725c29c506612003de393249b94310ea78ae44cb1d04b220095
# via weasyprint
pyphen==0.17.2 \
--hash=sha256:3a07fb017cb2341e1d9ff31b8634efb1ae4dc4b130468c7c39dd3d32e7c3affd \
@@ -442,9 +452,9 @@ pyyaml==6.0.3 \
--hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \
--hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0
# via pre-commit
tinycss2==1.4.0 \
--hash=sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7 \
--hash=sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289
tinycss2==1.5.1 \
--hash=sha256:3415ba0f5839c062696996998176c4a3751d18b7edaaeeb658c9ce21ec150661 \
--hash=sha256:d339d2b616ba90ccce58da8495a78f46e55d4d25f9fd71dfd526f07e7d53f957
# via
# cssselect2
# weasyprint
@@ -452,17 +462,17 @@ tinyhtml5==2.0.0 \
--hash=sha256:086f998833da24c300c414d9fe81d9b368fd04cb9d2596a008421cbc705fcfcc \
--hash=sha256:13683277c5b176d070f82d099d977194b7a1e26815b016114f581a74bbfbf47e
# via weasyprint
unoserver==3.4 \
--hash=sha256:3dcf2204013def1d1ddd3671f38b11346bdf349fef9728277462666a8a634419 \
--hash=sha256:64c24d33d4f65d680a2d9f676518cb28e7fd6c1f9d9a745c33e4a4cb59afdfcd
unoserver==3.6 \
--hash=sha256:25c360fa194396a89cb79b4edd2735f8e4f0fd8531e59db3952114585bd7df05 \
--hash=sha256:e446bcb3638c51880f002aaeecab1cf74dfa9df81035f027f7ff2e081b6d7015
# via -r .github/scripts/requirements_dev.in
virtualenv==20.35.4 \
--hash=sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c \
--hash=sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b
virtualenv==20.36.1 \
--hash=sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f \
--hash=sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba
# via pre-commit
weasyprint==66.0 \
--hash=sha256:82b0783b726fcd318e2c977dcdddca76515b30044bc7a830cc4fbe717582a6d0 \
--hash=sha256:da71dc87dc129ac9cffdc65e5477e90365ab9dbae45c744014ec1d06303dde40
weasyprint==68.0 \
--hash=sha256:447f40898b747cb44ac31a5d493d512e7441fd56e13f63744c099383bbf9cda9 \
--hash=sha256:c2cb40c71b50837c5971f00171c9e4078e8c9912dd7c217f3e90e068f11e8aa1
# via -r .github/scripts/requirements_dev.in
webencodings==0.5.1 \
--hash=sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78 \
@@ -471,86 +481,27 @@ webencodings==0.5.1 \
# cssselect2
# tinycss2
# tinyhtml5
zopfli==0.2.3.post1 \
--hash=sha256:0aa5f90d6298bda02a95bc8dc8c3c19004d5a4e44bda00b67ca7431d857b4b54 \
--hash=sha256:0cc20b02a9531559945324c38302fd4ba763311632d0ec8a1a0aa9c10ea363e6 \
--hash=sha256:1d8cc06605519e82b16df090e17cb3990d1158861b2872c3117f1168777b81e4 \
--hash=sha256:1f990634fd5c5c8ced8edddd8bd45fab565123b4194d6841e01811292650acae \
--hash=sha256:2345e713260a350bea0b01a816a469ea356bc2d63d009a0d777691ecbbcf7493 \
--hash=sha256:2768c877f76c8a0e7519b1c86c93757f3c01492ddde55751e9988afb7eff64e1 \
--hash=sha256:29ea74e72ffa6e291b8c6f2504ce6c146b4fe990c724c1450eb8e4c27fd31431 \
--hash=sha256:34a99592f3d9eb6f737616b5bd74b48a589fdb3cb59a01a50d636ea81d6af272 \
--hash=sha256:3654bfc927bc478b1c3f3ff5056ed7b20a1a37fa108ca503256d0a699c03bbb1 \
--hash=sha256:3657e416ffb8f31d9d3424af12122bb251befae109f2e271d87d825c92fc5b7b \
--hash=sha256:37d011e92f7b9622742c905fdbed9920a1d0361df84142807ea2a528419dea7f \
--hash=sha256:3827170de28faf144992d3d4dcf8f3998fe3c8a6a6f4a08f1d42c2ec6119d2bb \
--hash=sha256:39e576f93576c5c223b41d9c780bbb91fd6db4babf3223d2a4fe7bf568e2b5a8 \
--hash=sha256:3a89277ed5f8c0fb2d0b46d669aa0633123aa7381f1f6118c12f15e0fb48f8ca \
--hash=sha256:3c163911f8bad94b3e1db0a572e7c28ba681a0c91d0002ea1e4fa9264c21ef17 \
--hash=sha256:3f0197b6aa6eb3086ae9e66d6dd86c4d502b6c68b0ec490496348ae8c05ecaef \
--hash=sha256:48dba9251060289101343110ab47c0756f66f809bb4d1ddbb6d5c7e7752115c5 \
--hash=sha256:4915a41375bdee4db749ecd07d985a0486eb688a6619f713b7bf6fbfd145e960 \
--hash=sha256:4c1226a7e2c7105ac31503a9bb97454743f55d88164d6d46bc138051b77f609b \
--hash=sha256:4e50ffac74842c1c1018b9b73875a0d0a877c066ab06bf7cccbaa84af97e754f \
--hash=sha256:518f1f4ed35dd69ce06b552f84e6d081f07c552b4c661c5312d950a0b764a58a \
--hash=sha256:5aad740b4d4fcbaaae4887823925166ffd062db3b248b3f432198fc287381d1a \
--hash=sha256:5f272186e03ad55e7af09ab78055535c201b1a0bcc2944edb1768298d9c483a4 \
--hash=sha256:5fcfc0dc2761e4fcc15ad5d273b4d58c2e8e059d3214a7390d4d3c8e2aee644e \
--hash=sha256:60db20f06c3d4c5934b16cfa62a2cc5c3f0686bffe0071ed7804d3c31ab1a04e \
--hash=sha256:615a8ac9dda265e9cc38b2a76c3142e4a9f30fea4a79c85f670850783bc6feb4 \
--hash=sha256:6482db9876c68faac2d20a96b566ffbf65ddaadd97b222e4e73641f4f8722fc4 \
--hash=sha256:6617fb10f9e4393b331941861d73afb119cd847e88e4974bdbe8068ceef3f73f \
--hash=sha256:676919fba7311125244eb0c4393679ac5fe856e5864a15d122bd815205369fa0 \
--hash=sha256:6c2d2bc8129707e34c51f9352c4636ca313b52350bbb7e04637c46c1818a2a70 \
--hash=sha256:71390dbd3fbf6ebea9a5d85ffed8c26ee1453ee09248e9b88486e30e0397b775 \
--hash=sha256:716cdbfc57bfd3d3e31a58e6246e8190e6849b7dbb7c4ce39ef8bbf0edb8f6d5 \
--hash=sha256:75a26a2307b10745a83b660c404416e984ee6fca515ec7f0765f69af3ce08072 \
--hash=sha256:7be5cc6732eb7b4df17305d8a7b293223f934a31783a874a01164703bc1be6cd \
--hash=sha256:7cce242b5df12b2b172489daf19c32e5577dd2fac659eb4b17f6a6efb446fd5c \
--hash=sha256:81c341d9bb87a6dbbb0d45d6e272aca80c7c97b4b210f9b6e233bf8b87242f29 \
--hash=sha256:89899641d4de97dbad8e0cde690040d078b6aea04066dacaab98e0b5a23573f2 \
--hash=sha256:8d5ab297d660b75c159190ce6d73035502310e40fd35170aed7d1a1aea7ddd65 \
--hash=sha256:8fbe5bcf10d01aab3513550f284c09fef32f342b36f56bfae2120a9c4d12c130 \
--hash=sha256:91a2327a4d7e77471fa4fbb26991c6de4a738c6fc6a33e09bb25f56a870a4b7b \
--hash=sha256:95a260cafd56b8fffa679918937401c80bb38e1681c448b988022e4c3610965d \
--hash=sha256:96484dc0f48be1c5d7ae9f38ed1ce41e3675fd506b27c11a6607f14b49101e99 \
--hash=sha256:9a6aec38a989bad7ddd1ef53f1265699e49e294d08231b5313d61293f3cd6237 \
--hash=sha256:9ba214f4f45bec195ee8559651154d3ac2932470b9d91c5715fc29c013349f8c \
--hash=sha256:9f4a7ec2770e6af05f5a02733fd3900f30a9cd58e5d6d3727e14c5bcd6e7d587 \
--hash=sha256:a1cf720896d2ce998bc8e051d4b4ce0d8bec007aab6243102e8e1d22a0b2fb3f \
--hash=sha256:a241a68581d34d67b40c425cce3d1fd211c092f99d9250947824ccba9f491949 \
--hash=sha256:a53b18797cdef27e019db595d66c4b077325afe2fd62145953275f53d84ce40c \
--hash=sha256:a82fc2dbebe6eb908b9c665e71496f8525c1bc4d2e3a7a7722ef2b128b6227c8 \
--hash=sha256:a86eb88e06bd87e1fff31dac878965c26b0c26db59ddcf78bb0379a954b120de \
--hash=sha256:aa588b21044f8a74e423d8c8a4c7fc9988501878aacced793467010039c50734 \
--hash=sha256:b05296e8bc88c92e2b21e0a9bae4740c1551ee613c1d93a51fd28a7a0b2b6fbb \
--hash=sha256:b0ec13f352ea5ae0fc91f98a48540512eed0767d0ec4f7f3cb92d92797983d18 \
--hash=sha256:b3df42f52502438ee973042cc551877d24619fa1cd38ef7b7e9ac74200daca8b \
--hash=sha256:b78008a69300d929ca2efeffec951b64a312e9a811e265ea4a907ab546d79fa6 \
--hash=sha256:b9026a21b6d41eb0e2e63f5bc1242c3fcc43ecb770963cda99a4307863dac12e \
--hash=sha256:bbe429fc50686bb2a2608a30843e36fbaa123462a5284f136c7d9e0145220bfd \
--hash=sha256:bfa1eb759e07d8b7aa7a310a2bc535e127ee70addf90dc8d4b946b593c3e51a8 \
--hash=sha256:c1e0ed5d84ffa2d677cc9582fc01e61dab2e7ef8b8996e055f0a76167b1b94df \
--hash=sha256:c4278d1873ce6e803e5d4f8d702fd3026bd67fca744aa98881324d1157ddf748 \
--hash=sha256:cac2b37ab21c2b36a10b685b1893ebd6b0f83ae26004838ac817680881576567 \
--hash=sha256:cbe6df25807227519debd1a57ab236f5f6bad441500e85b13903e51f93a43214 \
--hash=sha256:cd2c002f160502608dcc822ed2441a0f4509c52e86fcfd1a09e937278ed1ca14 \
--hash=sha256:e0137dd64a493ba6a4be37405cfd6febe650a98cc1e9dca8f6b8c63b1db11b41 \
--hash=sha256:e63d558847166543c2c9789e6f985400a520b7eacc4b99181668b2c3aeadd352 \
--hash=sha256:eb45a34f23da4f8bc712b6376ca5396914b0b7c09adbb001dad964eb7f3132f8 \
--hash=sha256:ecb7572df5372abce8073df078207d9d1749f20b8b136089916a4a0868d56051 \
--hash=sha256:f12000a6accdd4bf0a3fa6eaa1b1c7a7bc80af0a2edf3f89d770d3dcce1d0e22 \
--hash=sha256:f7d69c1a7168ad0e9cb864e8663acb232986a0c9c9cb9801f56bf6214f53a54d \
--hash=sha256:f815fcc2b2a457977724bad97fb4854022980f51ce7b136925e336b530545ae1 \
--hash=sha256:fc39f5c27f962ec8660d8d20c24762431131b5d8c672b44b0a54cf2b5bcde9b9
zopfli==0.4.0 \
--hash=sha256:03181d48e719fcb6cf8340189c61e8f9883d8bbbdf76bf5212a74457f7d083c1 \
--hash=sha256:18b5f1570f64d4988482e4466f10ef5f2a30f687c19ad62a64560f2152dc89eb \
--hash=sha256:25e4863b8dc30e5d5309f87c106b0b7d3da4ed0e340b8a52b36d4471e797589f \
--hash=sha256:7d66337be6d5613dec55213e9ac28f378c41e2cc04fbad4a10748e4df774ca85 \
--hash=sha256:9097e8e1dfdb7f5aea5464e469946857e80502b6d29ba1b232450916bd4a74d1 \
--hash=sha256:a8ee992b2549e090cd3f0178bf606dd41a29e0613a04cdf5054224662c72dce6 \
--hash=sha256:b72a010d205d00b2855acc2302772067362f9ab5a012e3550662aec60d28e6b3 \
--hash=sha256:b8bdb41fbfdc4738b7bdc09ed7c1e951579fae192391a5e694d59bb186cdbec7 \
--hash=sha256:c3ba02a9a6ca90481d2b2f68bab038b310d63a1e3b5ae305e95a6599787ed941 \
--hash=sha256:d1b98ad47c434ef213444a03ef2f826eeec100144d64f6a57504b9893d3931ce \
--hash=sha256:f67d04280065e24cb9a4174cb6b3d1f763687f8cb2963aa135ad8f57c6995f5a \
--hash=sha256:f94e4dd7d76b4fe9f5d9229372be20d7f786164eea5152d1af1c34298c3d5975
# via fonttools
# The following packages are considered to be unsafe in a requirements file:
pip==25.3 \
--hash=sha256:8d0538dbbd7babbd207f261ed969c65de439f6bc9e5dbd3b3b9a77f25d95f343 \
--hash=sha256:9655943313a94722b7774661c21049070f6bbb0a1516bf02f7c8d5d9201514cd
pip==26.0 \
--hash=sha256:3ce220a0a17915972fbf1ab451baae1521c4539e778b28127efa79b974aff0fa \
--hash=sha256:98436feffb9e31bc9339cf369fd55d3331b1580b6a6f1173bacacddcf9c34754
# via -r .github/scripts/requirements_dev.in
setuptools==80.9.0 \
--hash=sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922 \
--hash=sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c
setuptools==80.10.2 \
--hash=sha256:8b0e9d10c784bf7d262c4e5ec5d4ec94127ce206e8738f29a437945fbc219b70 \
--hash=sha256:95b30ddfb717250edb492926c92b5221f7ef3fbcc2b07579bcd4a27da21d0173
# via -r .github/scripts/requirements_dev.in
+18 -18
View File
@@ -12,25 +12,25 @@ distlib==0.4.0 \
--hash=sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16 \
--hash=sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d
# via virtualenv
filelock==3.20.0 \
--hash=sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2 \
--hash=sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4
filelock==3.20.3 \
--hash=sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1 \
--hash=sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1
# via virtualenv
identify==2.6.15 \
--hash=sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757 \
--hash=sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf
identify==2.6.16 \
--hash=sha256:391ee4d77741d994189522896270b787aed8670389bfd60f326d677d64a6dfb0 \
--hash=sha256:846857203b5511bbe94d5a352a48ef2359532bc8f6727b5544077a0dcfb24980
# via pre-commit
nodeenv==1.9.1 \
--hash=sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f \
--hash=sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9
nodeenv==1.10.0 \
--hash=sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827 \
--hash=sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb
# via pre-commit
platformdirs==4.5.0 \
--hash=sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312 \
--hash=sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3
platformdirs==4.5.1 \
--hash=sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda \
--hash=sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31
# via virtualenv
pre-commit==4.5.0 \
--hash=sha256:25e2ce09595174d9c97860a95609f9f852c0614ba602de3561e267547f2335e1 \
--hash=sha256:dc5a065e932b19fc1d4c653c6939068fe54325af8e741e74e88db4d28a4dd66b
pre-commit==4.5.1 \
--hash=sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77 \
--hash=sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61
# via -r .github/scripts/requirements_pre_commit.in
pyyaml==6.0.3 \
--hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \
@@ -107,7 +107,7 @@ pyyaml==6.0.3 \
--hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \
--hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0
# via pre-commit
virtualenv==20.35.4 \
--hash=sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c \
--hash=sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b
virtualenv==20.36.1 \
--hash=sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f \
--hash=sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba
# via pre-commit
+3 -3
View File
@@ -8,7 +8,7 @@ tomli-w==1.2.0 \
--hash=sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90 \
--hash=sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021
# via -r .github/scripts/requirements_sync_readme.in
tomlkit==0.13.3 \
--hash=sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1 \
--hash=sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0
tomlkit==0.14.0 \
--hash=sha256:592064ed85b40fa213469f81ac584f67a4f2992509a7c3ea2d632208623a3680 \
--hash=sha256:cf00efca415dbd57575befb1f6634c4f42d2d87dbba376128adb42c121b87064
# via -r .github/scripts/requirements_sync_readme.in
+36 -86
View File
@@ -116,7 +116,7 @@ jobs:
egress-policy: audit
- name: Checkout main repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
repository: ${{ github.repository }}
ref: main
@@ -172,7 +172,7 @@ jobs:
return newComment.id;
- name: Checkout PR
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
repository: ${{ needs.check-pr.outputs.pr_repository }}
ref: ${{ needs.check-pr.outputs.pr_ref }}
@@ -194,81 +194,44 @@ jobs:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_API }}
- name: Get commit hashes for frontend and backend
id: commit-hashes
- name: Get commit hash for app
id: commit-hash
run: |
# Get last commit that touched the frontend folder, docker/frontend, or docker/compose
FRONTEND_HASH=$(git log -1 --format="%H" -- frontend/ docker/frontend/ docker/compose/ 2>/dev/null || echo "")
if [ -z "$FRONTEND_HASH" ]; then
FRONTEND_HASH="no-frontend-changes"
# Get last commit that touched the application code
APP_HASH=$(git log -1 --format="%H" -- . 2>/dev/null || echo "")
if [ -z "$APP_HASH" ]; then
APP_HASH="no-changes"
fi
# Get last commit that touched backend code, docker/backend, or docker/compose
BACKEND_HASH=$(git log -1 --format="%H" -- app/ docker/backend/ docker/compose/ 2>/dev/null || echo "")
if [ -z "$BACKEND_HASH" ]; then
BACKEND_HASH="no-backend-changes"
fi
echo "App hash: $APP_HASH"
echo "app_hash=$APP_HASH" >> $GITHUB_OUTPUT
echo "Frontend hash: $FRONTEND_HASH"
echo "Backend hash: $BACKEND_HASH"
echo "frontend_hash=$FRONTEND_HASH" >> $GITHUB_OUTPUT
echo "backend_hash=$BACKEND_HASH" >> $GITHUB_OUTPUT
# Short hashes for tags
if [ "$FRONTEND_HASH" = "no-frontend-changes" ]; then
echo "frontend_short=no-frontend" >> $GITHUB_OUTPUT
# Short hash for tags
if [ "$APP_HASH" = "no-changes" ]; then
echo "app_short=no-changes" >> $GITHUB_OUTPUT
else
echo "frontend_short=${FRONTEND_HASH:0:8}" >> $GITHUB_OUTPUT
echo "app_short=${APP_HASH:0:8}" >> $GITHUB_OUTPUT
fi
if [ "$BACKEND_HASH" = "no-backend-changes" ]; then
echo "backend_short=no-backend" >> $GITHUB_OUTPUT
else
echo "backend_short=${BACKEND_HASH:0:8}" >> $GITHUB_OUTPUT
fi
- name: Check if frontend image exists
id: check-frontend
- name: Check if image exists
id: check-image
run: |
if docker manifest inspect ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-frontend-${{ steps.commit-hashes.outputs.frontend_short }} >/dev/null 2>&1; then
if docker manifest inspect ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-${{ steps.commit-hash.outputs.app_short }} >/dev/null 2>&1; then
echo "exists=true" >> $GITHUB_OUTPUT
echo "Frontend image already exists, skipping build"
echo "Image already exists, skipping build"
else
echo "exists=false" >> $GITHUB_OUTPUT
echo "Frontend image needs to be built"
echo "Image needs to be built"
fi
- name: Check if backend image exists
id: check-backend
run: |
if docker manifest inspect ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-backend-${{ steps.commit-hashes.outputs.backend_short }} >/dev/null 2>&1; then
echo "exists=true" >> $GITHUB_OUTPUT
echo "Backend image already exists, skipping build"
else
echo "exists=false" >> $GITHUB_OUTPUT
echo "Backend image needs to be built"
fi
- name: Build and push V2 frontend image
if: steps.check-frontend.outputs.exists == 'false'
- name: Build and push V2 image
if: steps.check-image.outputs.exists == 'false'
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
context: .
file: ./docker/frontend/Dockerfile
file: ./docker/embedded/Dockerfile
push: true
tags: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-frontend-${{ steps.commit-hashes.outputs.frontend_short }}
build-args: VERSION_TAG=v2-alpha
platforms: linux/amd64
- name: Build and push V2 backend image
if: steps.check-backend.outputs.exists == 'false'
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
context: .
file: ./docker/backend/Dockerfile
push: true
tags: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-backend-${{ steps.commit-hashes.outputs.backend_short }}
tags: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-${{ steps.commit-hash.outputs.app_short }}
build-args: VERSION_TAG=v2-alpha
platforms: linux/amd64
@@ -283,17 +246,16 @@ jobs:
run: |
# Use same port strategy as regular PRs - just the PR number
V2_PORT=${{ needs.check-pr.outputs.pr_number }}
BACKEND_PORT=$((V2_PORT + 10000)) # Backend on higher port to avoid conflicts
# Create docker-compose for V2 with separate frontend and backend
# Create docker-compose for V2 with unified embedded image
cat > docker-compose.yml << EOF
version: '3.3'
services:
stirling-pdf-v2-backend:
container_name: stirling-pdf-v2-backend-pr-${{ needs.check-pr.outputs.pr_number }}
image: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-backend-${{ steps.commit-hashes.outputs.backend_short }}
stirling-pdf-v2:
container_name: stirling-pdf-v2-pr-${{ needs.check-pr.outputs.pr_number }}
image: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-${{ steps.commit-hash.outputs.app_short }}
ports:
- "${BACKEND_PORT}:8080" # Backend API port
- "${V2_PORT}:8080"
volumes:
- /stirling/V2-PR-${{ needs.check-pr.outputs.pr_number }}/data:/usr/share/tessdata:rw
- /stirling/V2-PR-${{ needs.check-pr.outputs.pr_number }}/config:/configs:rw
@@ -305,7 +267,7 @@ jobs:
SECURITY_INITIALLOGIN_PASSWORD: "${{ secrets.TEST_LOGIN_PASSWORD }}"
SYSTEM_DEFAULTLOCALE: en-GB
UI_APPNAME: "Stirling-PDF V2 PR#${{ needs.check-pr.outputs.pr_number }}"
UI_HOMEDESCRIPTION: "V2 PR#${{ needs.check-pr.outputs.pr_number }} - Frontend/Backend Split Architecture"
UI_HOMEDESCRIPTION: "V2 PR#${{ needs.check-pr.outputs.pr_number }} - Embedded Architecture"
UI_APPNAMENAVBAR: "V2 PR#${{ needs.check-pr.outputs.pr_number }}"
SYSTEM_MAXFILESIZE: "100"
METRICS_ENABLED: "true"
@@ -313,17 +275,6 @@ jobs:
SWAGGER_SERVER_URL: "https://${V2_PORT}.ssl.stirlingpdf.cloud"
baseUrl: "https://${V2_PORT}.ssl.stirlingpdf.cloud"
restart: on-failure:5
stirling-pdf-v2-frontend:
container_name: stirling-pdf-v2-frontend-pr-${{ needs.check-pr.outputs.pr_number }}
image: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-frontend-${{ steps.commit-hashes.outputs.frontend_short }}
ports:
- "${V2_PORT}:80" # Frontend port (same as regular PRs)
environment:
VITE_API_BASE_URL: "http://${{ secrets.NEW_VPS_HOST }}:${BACKEND_PORT}"
depends_on:
- stirling-pdf-v2-backend
restart: on-failure:5
EOF
# Deploy to VPS
@@ -339,15 +290,15 @@ jobs:
# Stop any existing container and clean up
cd /stirling/V2-PR-${{ needs.check-pr.outputs.pr_number }}
docker-compose down --remove-orphans 2>/dev/null || true
# Start the new container
docker-compose pull
docker-compose up -d
# Clean up unused Docker resources to save space
docker system prune -af --volumes || true
# Clean up old backend/frontend images (older than 2 weeks)
# Clean up old images (older than 2 weeks)
docker image prune -af --filter "until=336h" --filter "label!=keep=true" || true
ENDSSH
@@ -383,7 +334,7 @@ jobs:
const httpsUrl = `https://${v2Port}.ssl.stirlingpdf.cloud`;
const commentBody = `## 🚀 V2 Auto-Deployment Complete!\n\n` +
`Your V2 PR with the new frontend/backend split architecture has been deployed!\n\n` +
`Your V2 PR with embedded architecture has been deployed!\n\n` +
`🔗 **Direct Test URL (non-SSL)** [${deploymentUrl}](${deploymentUrl})\n\n` +
`🔐 **Secure HTTPS URL**: [${httpsUrl}](${httpsUrl})\n\n` +
`_This deployment will be automatically cleaned up when the PR is closed._\n\n` +
@@ -411,7 +362,7 @@ jobs:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup GitHub App Bot
if: github.actor != 'dependabot[bot]'
@@ -473,9 +424,8 @@ jobs:
# Remove V2 PR-specific directories
rm -rf /stirling/V2-PR-${{ github.event.pull_request.number }}
# Clean up V2 containers by name (in case compose cleanup missed them)
docker rm -f stirling-pdf-v2-frontend-pr-${{ github.event.pull_request.number }} || true
docker rm -f stirling-pdf-v2-backend-pr-${{ github.event.pull_request.number }} || true
# Clean up V2 container by name (in case compose cleanup missed it)
docker rm -f stirling-pdf-v2-pr-${{ github.event.pull_request.number }} || true
echo "V2 cleanup completed"
else
@@ -45,7 +45,7 @@ jobs:
egress-policy: audit
- name: Checkout PR
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup GitHub App Bot
if: github.actor != 'dependabot[bot]'
@@ -133,7 +133,7 @@ jobs:
egress-policy: audit
- name: Checkout PR
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup GitHub App Bot
if: github.actor != 'dependabot[bot]'
@@ -145,7 +145,7 @@ jobs:
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- name: Checkout PR
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
ref: refs/pull/${{ needs.check-comment.outputs.pr_number }}/merge
token: ${{ steps.setup-bot.outputs.token }}
@@ -153,9 +153,14 @@ jobs:
- name: Set up JDK 21
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
with:
java-version: "17"
java-version: "21"
distribution: "temurin"
- name: Setup Gradle
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
with:
gradle-version: 8.14
- name: Run Gradle Command
run: |
if [ "${{ needs.check-comment.outputs.disable_security }}" == "true" ]; then
@@ -163,8 +168,11 @@ jobs:
else
export DISABLE_ADDITIONAL_FEATURES=false
fi
./gradlew clean build
./gradlew build
env:
MAVEN_USER: ${{ secrets.MAVEN_USER }}
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
STIRLING_PDF_DESKTOP_UI: false
- name: Set up Docker Buildx
@@ -367,7 +375,7 @@ jobs:
egress-policy: audit
- name: Check out the repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup GitHub App Bot
id: setup-bot
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
egress-policy: audit
- name: Checkout PR
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup GitHub App Bot
if: github.actor != 'dependabot[bot]'
+1 -1
View File
@@ -23,7 +23,7 @@ jobs:
with:
egress-policy: audit
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
fetch-depth: 0
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
with:
egress-policy: audit
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup GitHub App Bot
id: setup-bot
+52 -10
View File
@@ -36,7 +36,7 @@ jobs:
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Check for file changes
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
@@ -60,7 +60,7 @@ jobs:
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Set up JDK ${{ matrix.jdk-version }}
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
@@ -68,9 +68,17 @@ jobs:
java-version: ${{ matrix.jdk-version }}
distribution: "temurin"
- name: Setup Gradle
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
with:
gradle-version: 8.14
- name: Build with Gradle and spring security ${{ matrix.spring-security }}
run: ./gradlew clean build -PnoSpotless
run: ./gradlew build -PnoSpotless
env:
MAVEN_USER: ${{ secrets.MAVEN_USER }}
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
DISABLE_ADDITIONAL_FEATURES: ${{ matrix.spring-security }}
- name: Check Test Reports Exist
@@ -127,7 +135,7 @@ jobs:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Set up JDK 21
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
@@ -135,9 +143,17 @@ jobs:
java-version: "21"
distribution: "temurin"
- name: Setup Gradle
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
with:
gradle-version: 8.14
- name: Generate OpenAPI documentation
run: ./gradlew :stirling-pdf:generateOpenApiDocs
env:
MAVEN_USER: ${{ secrets.MAVEN_USER }}
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
DISABLE_ADDITIONAL_FEATURES: true
- name: Upload OpenAPI Documentation
@@ -156,7 +172,7 @@ jobs:
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Set up Node.js
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
@@ -191,7 +207,7 @@ jobs:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Set up JDK 21
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
@@ -199,8 +215,17 @@ jobs:
java-version: "21"
distribution: "temurin"
- name: Setup Gradle
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
with:
gradle-version: 8.14
- name: check the licenses for compatibility
run: ./gradlew clean checkLicense
run: ./gradlew checkLicense
env:
MAVEN_USER: ${{ secrets.MAVEN_USER }}
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
- name: FAILED - check the licenses for compatibility
if: failure()
@@ -236,7 +261,7 @@ jobs:
egress-policy: audit
- name: Checkout Repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Set up JDK 21
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
@@ -244,6 +269,11 @@ jobs:
java-version: "21"
distribution: "temurin"
- name: Setup Gradle
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
with:
gradle-version: 8.14
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
@@ -269,6 +299,10 @@ jobs:
chmod +x ./testing/test.sh
chmod +x ./testing/test_disabledEndpoints.sh
./testing/test.sh
env:
MAVEN_USER: ${{ secrets.MAVEN_USER }}
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
test-build-docker-images:
if: github.event_name == 'pull_request' && needs.files-changed.outputs.project == 'true'
@@ -291,7 +325,7 @@ jobs:
egress-policy: audit
- name: Checkout Repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Free disk space on runner
run: |
@@ -306,9 +340,17 @@ jobs:
java-version: "21"
distribution: "temurin"
- name: Setup Gradle
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
with:
gradle-version: 8.14
- name: Build application
run: ./gradlew clean build
run: ./gradlew build
env:
MAVEN_USER: ${{ secrets.MAVEN_USER }}
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
DISABLE_ADDITIONAL_FEATURES: true
STIRLING_PDF_DESKTOP_UI: false
+1 -1
View File
@@ -32,7 +32,7 @@ jobs:
egress-policy: audit
- name: Checkout main branch first
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup GitHub App Bot
id: setup-bot
+1 -1
View File
@@ -22,7 +22,7 @@ jobs:
egress-policy: audit
- name: "Checkout Repository"
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: "Dependency Review"
uses: actions/dependency-review-action@3c4e3dcb1aa7874d2c16be7d79418e9b7efd6261 # v4.8.2
with:
@@ -30,7 +30,7 @@ jobs:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Check for file changes
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
@@ -54,7 +54,7 @@ jobs:
egress-policy: audit
- name: Checkout PR head (default)
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
fetch-depth: 0
persist-credentials: false
@@ -69,7 +69,7 @@ jobs:
- name: Checkout BASE branch (safe script)
if: github.event_name == 'pull_request'
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
ref: ${{ github.event.pull_request.base.sha }}
path: base
@@ -317,7 +317,7 @@ jobs:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
fetch-depth: 0
persist-credentials: false
@@ -336,11 +336,19 @@ jobs:
java-version: "21"
distribution: "temurin"
- name: Setup Gradle
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
with:
gradle-version: 8.14
- name: Check licenses and generate report
id: license-check
run: |
./gradlew clean checkLicense generateLicenseReport || echo "LICENSE_CHECK_FAILED=true" >> $GITHUB_ENV
./gradlew checkLicense generateLicenseReport || echo "LICENSE_CHECK_FAILED=true" >> $GITHUB_ENV
env:
MAVEN_USER: ${{ secrets.MAVEN_USER }}
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
DISABLE_ADDITIONAL_FEATURES: false
STIRLING_PDF_DESKTOP_UI: true
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
egress-policy: audit
- name: Check out the repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Run Labeler
uses: crazy-max/ghaction-github-labeler@24d110aa46a59976b8a7f35518cb7f14f434c916 # v5.3.0
+33 -16
View File
@@ -42,7 +42,7 @@ jobs:
with:
egress-policy: audit
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Set up JDK 21
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
@@ -50,7 +50,8 @@ jobs:
java-version: "21"
distribution: "temurin"
- uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
- name: Setup Gradle
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
with:
gradle-version: 8.14
@@ -62,6 +63,10 @@ jobs:
VERSION=$(./gradlew printVersion --quiet | tail -1)
echo "Extracted version: $VERSION"
echo "versionNumber=$VERSION" >> $GITHUB_OUTPUT
env:
MAVEN_USER: ${{ secrets.MAVEN_USER }}
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
- name: Determine build matrix
id: set-matrix
@@ -110,7 +115,7 @@ jobs:
with:
egress-policy: audit
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Set up JDK 21
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
@@ -118,21 +123,25 @@ jobs:
java-version: "21"
distribution: "temurin"
- uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
- name: Setup Gradle
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
with:
gradle-version: 8.14
- name: Setup Node.js
if: matrix.variant.build_frontend == true
uses: actions/setup-node@v4
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22
cache: "npm"
cache-dependency-path: frontend/package-lock.json
- name: Build JAR
run: ./gradlew clean build ${{ matrix.variant.build_frontend && '-PbuildWithFrontend=true' || '' }} -x spotlessApply -x spotlessCheck -x test -x sonarqube
run: ./gradlew build ${{ matrix.variant.build_frontend && '-PbuildWithFrontend=true' || '' }} -x spotlessApply -x spotlessCheck -x test -x sonarqube
env:
MAVEN_USER: ${{ secrets.MAVEN_USER }}
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
DISABLE_ADDITIONAL_FEATURES: ${{ matrix.variant.disable_security }}
STIRLING_PDF_DESKTOP_UI: false
@@ -167,7 +176,7 @@ jobs:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Install dependencies (ubuntu only)
if: matrix.platform == 'ubuntu-22.04'
@@ -176,14 +185,14 @@ jobs:
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libjavascriptcoregtk-4.0-dev libsoup2.4-dev libjavascriptcoregtk-4.1-dev libsoup-3.0-dev
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22
cache: "npm"
cache-dependency-path: frontend/package-lock.json
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
uses: dtolnay/rust-toolchain@4be9e76fd7c4901c61fb841f559994984270fce7 # stable
with:
toolchain: stable
targets: ${{ (matrix.platform == 'macos-15' || matrix.platform == 'macos-15-intel') && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
@@ -194,13 +203,18 @@ jobs:
java-version: "21"
distribution: "temurin"
- name: Setup Gradle
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
with:
gradle-version: 8.14
- name: Build Java backend with JLink
working-directory: ./
shell: bash
run: |
chmod +x ./gradlew
echo "🔧 Building Stirling-PDF JAR..."
./gradlew clean build -x spotlessApply -x spotlessCheck -x test -x sonarqube
./gradlew build -x spotlessApply -x spotlessCheck -x test -x sonarqube
# Find the built JAR
STIRLING_JAR=$(ls app/core/build/libs/stirling-pdf-*.jar | head -n 1)
@@ -264,6 +278,9 @@ jobs:
RUNTIME_SIZE=$(du -sh ./frontend/src-tauri/runtime/jre | cut -f1)
echo "📊 Custom JRE size: $RUNTIME_SIZE"
env:
MAVEN_USER: ${{ secrets.MAVEN_USER }}
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
DISABLE_ADDITIONAL_FEATURES: true
- name: Install frontend dependencies
@@ -274,7 +291,7 @@ jobs:
- name: Setup DigiCert KeyLocker
id: digicert-setup
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && (github.event_name == 'release' || github.ref == 'refs/heads/V2-master') }}
uses: digicert/ssm-code-signing@v1.1.0
uses: digicert/ssm-code-signing@9476ceec3ea1c63298d4403b983e1ccf2556ff4c # v1.1.0
env:
SM_API_KEY: ${{ secrets.SM_API_KEY }}
SM_CLIENT_CERT_FILE_B64: ${{ secrets.SM_CLIENT_CERT_FILE_B64 }}
@@ -382,7 +399,7 @@ jobs:
echo "Certificate imported successfully."
- name: Build Tauri app
uses: tauri-apps/tauri-action@v0
uses: tauri-apps/tauri-action@51a9f1156b33df106d827c3a78f8f894946c5faa # v0.5.25
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
@@ -535,25 +552,25 @@ jobs:
egress-policy: audit
- name: Download all Tauri artifacts
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
pattern: Stirling-PDF-*
path: ./artifacts/tauri
- name: Download JAR artifact (default)
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
name: jar
path: ./artifacts/jars
- name: Download JAR artifact (with login)
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
name: jar-with-login
path: ./artifacts/jars
- name: Download JAR artifact (server only)
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
name: jar-server
path: ./artifacts/jars
+11 -2
View File
@@ -26,7 +26,7 @@ jobs:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
fetch-depth: 0
@@ -57,8 +57,17 @@ jobs:
java-version: 21
distribution: "temurin"
- name: Setup Gradle
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
with:
gradle-version: 8.14
- name: Build with Gradle
run: ./gradlew clean build
run: ./gradlew build
env:
MAVEN_USER: ${{ secrets.MAVEN_USER }}
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
- name: git add
run: |
+13 -2
View File
@@ -37,7 +37,7 @@ jobs:
with:
egress-policy: audit
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Set up JDK 21
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
@@ -45,7 +45,8 @@ jobs:
java-version: "21"
distribution: "temurin"
- uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
- name: Setup Gradle
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
with:
gradle-version: 8.14
@@ -56,6 +57,16 @@ jobs:
- name: Get version number
id: versionNumber
run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT
env:
MAVEN_USER: ${{ secrets.MAVEN_USER }}
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
- name: Install cosign
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/V2-master'
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
with:
cosign-release: "v2.4.1"
- name: Install cosign
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/V2-master'
+1 -1
View File
@@ -40,7 +40,7 @@ jobs:
egress-policy: audit
- name: "Checkout code"
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
+12 -2
View File
@@ -31,7 +31,7 @@ jobs:
with:
egress-policy: audit
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Set up JDK 21
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
@@ -39,7 +39,10 @@ jobs:
java-version: "21"
distribution: "temurin"
- uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
- name: Setup Gradle
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
with:
gradle-version: 8.14
- name: Generate Swagger documentation
run: ./gradlew :stirling-pdf:generateOpenApiDocs
@@ -47,12 +50,19 @@ jobs:
- name: Upload Swagger Documentation to SwaggerHub
run: ./gradlew swaggerhubUpload
env:
MAVEN_USER: ${{ secrets.MAVEN_USER }}
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
SWAGGERHUB_API_KEY: ${{ secrets.SWAGGERHUB_API_KEY }}
SWAGGERHUB_USER: "Frooodle"
- name: Get version number
id: versionNumber
run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT
env:
MAVEN_USER: ${{ secrets.MAVEN_USER }}
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
- name: Set API version as published and default on SwaggerHub
run: |
+4 -1
View File
@@ -7,6 +7,9 @@ on:
- main
paths:
- "build.gradle"
- "app/common/build.gradle"
- "app/core/build.gradle"
- "app/proprietary/build.gradle"
- "README.md"
- "frontend/public/locales/*/translation.toml"
- "app/core/src/main/resources/static/3rdPartyLicenses.json"
@@ -36,7 +39,7 @@ jobs:
with:
egress-policy: audit
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup GitHub App Bot
id: setup-bot
+25 -7
View File
@@ -33,6 +33,11 @@ jobs:
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
with:
egress-policy: audit
- name: Determine build matrix
id: set-matrix
run: |
@@ -72,7 +77,7 @@ jobs:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Install dependencies (ubuntu only)
if: matrix.platform == 'ubuntu-22.04'
@@ -81,14 +86,14 @@ jobs:
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libjavascriptcoregtk-4.0-dev libsoup2.4-dev libjavascriptcoregtk-4.1-dev libsoup-3.0-dev
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22
cache: "npm"
cache-dependency-path: frontend/package-lock.json
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
uses: dtolnay/rust-toolchain@4be9e76fd7c4901c61fb841f559994984270fce7 # stable
with:
toolchain: stable
targets: ${{ (matrix.platform == 'macos-15' || matrix.platform == 'macos-15-intel') && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
@@ -99,14 +104,19 @@ jobs:
java-version: "21"
distribution: "temurin"
- name: Setup Gradle
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
with:
gradle-version: 8.14
- name: Build Java backend with JLink
working-directory: ./
shell: bash
run: |
chmod +x ./gradlew
echo "🔧 Building Stirling-PDF JAR..."
# STIRLING_PDF_DESKTOP_UI=false ./gradlew clean bootJar --no-daemon
./gradlew clean build -x spotlessApply -x spotlessCheck -x test -x sonarqube
# STIRLING_PDF_DESKTOP_UI=false ./gradlew bootJar --no-daemon
./gradlew build -x spotlessApply -x spotlessCheck -x test -x sonarqube
# Find the built JAR
STIRLING_JAR=$(ls app/core/build/libs/stirling-pdf-*.jar | head -n 1)
@@ -170,6 +180,9 @@ jobs:
RUNTIME_SIZE=$(du -sh ./frontend/src-tauri/runtime/jre | cut -f1)
echo "📊 Custom JRE size: $RUNTIME_SIZE"
env:
MAVEN_USER: ${{ secrets.MAVEN_USER }}
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
DISABLE_ADDITIONAL_FEATURES: true
- name: Install frontend dependencies
@@ -180,7 +193,7 @@ jobs:
- name: Setup DigiCert KeyLocker
id: digicert-setup
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' }}
uses: digicert/ssm-code-signing@v1.1.0
uses: digicert/ssm-code-signing@9476ceec3ea1c63298d4403b983e1ccf2556ff4c # v1.1.0
env:
SM_API_KEY: ${{ secrets.SM_API_KEY }}
SM_CLIENT_CERT_FILE_B64: ${{ secrets.SM_CLIENT_CERT_FILE_B64 }}
@@ -299,7 +312,7 @@ jobs:
ls -la /usr/bin/hd* || echo "No hd* tools found"
- name: Build Tauri app
uses: tauri-apps/tauri-action@v0
uses: tauri-apps/tauri-action@51a9f1156b33df106d827c3a78f8f894946c5faa # v0.5.25
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
@@ -631,6 +644,11 @@ jobs:
runs-on: ubuntu-latest
if: always()
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
with:
egress-policy: audit
- name: Report build results
run: |
if [ "${{ needs.build.result }}" = "success" ]; then
+12 -4
View File
@@ -30,7 +30,7 @@ jobs:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Set up JDK 21
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
@@ -44,8 +44,11 @@ jobs:
gradle-version: 8.14
- name: Build with Gradle
run: ./gradlew clean build
run: ./gradlew build
env:
MAVEN_USER: ${{ secrets.MAVEN_USER }}
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
DISABLE_ADDITIONAL_FEATURES: true
- name: Set up Docker Buildx
@@ -125,7 +128,12 @@ jobs:
outputs:
frontend: ${{ steps.changes.outputs.frontend }}
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
with:
egress-policy: audit
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Check for file changes
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
@@ -144,7 +152,7 @@ jobs:
with:
egress-policy: audit
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Set up Node
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
+1 -1
View File
@@ -213,7 +213,7 @@ id_ed25519.pub
.mypy_cache
.pytest_cache
.ipynb_checkpoints
.build-cache
**/jcef-bundle/
+4 -12
View File
@@ -1,6 +1,6 @@
# Adding New React Tools to Stirling PDF
This guide covers how to add new PDF tools to the React frontend, either by migrating existing Thymeleaf templates or creating entirely new tools.
This guide covers how to add new PDF tools to the React frontend.
## Overview
@@ -188,7 +188,7 @@ import { use[ToolName]Tips } from "../components/tooltips/use[ToolName]Tips";
const [ToolName] = (props: BaseToolProps) => {
const tips = use[ToolName]Tips();
// In your steps array:
steps: [
{
@@ -257,22 +257,14 @@ Update translation files. **Important: Only update `en-GB` files** - other langu
- Add `options.*` keys if your tool has settings with descriptions
**Tooltip Writing Guidelines:**
- **Use simple, everyday language** - avoid technical terms like "converts interactive elements"
- **Use simple, everyday language** - avoid technical terms like "converts interactive elements"
- **Focus on benefits** - explain what the user gains, not how it works internally
- **Use concrete examples** - "text boxes become regular text" vs "form fields are flattened"
- **Answer user questions** - "What does this do?", "When should I use this?", "What's this option for?"
- **Keep descriptions concise** - 1-2 sentences maximum per section
- **Use bullet points** for multiple benefits or features
## 6. Migration from Thymeleaf
When migrating existing Thymeleaf templates:
1. **Identify Form Parameters**: Look at the original `<form>` inputs to determine parameter structure
2. **Extract Translation Keys**: Find `#{key.name}` references and add them to JSON translations (For many tools these translations will already exist but some parts will be missing)
3. **Map API Endpoint**: Note the `th:action` URL for the operation hook
4. **Preserve Functionality**: Ensure all original form behaviour is replicated which is applicable to V2 react UI
## 7. Testing Your Tool
## 6. Testing Your Tool
- Verify tool appears in UI with correct icon and description
- Test with various file sizes and types
- Confirm translations work
+2 -5
View File
@@ -128,8 +128,8 @@ return useToolOperation({
## Architecture Overview
### Project Structure
- **Backend**: Spring Boot application with Thymeleaf templating
- **Frontend**: React-based SPA in `/frontend` directory (Thymeleaf templates fully replaced)
- **Backend**: Spring Boot application
- **Frontend**: React-based SPA in `/frontend` directory
- **File Storage**: IndexedDB for client-side file persistence and thumbnails
- **Internationalization**: JSON-based translations (converted from backend .properties)
- **PDF Processing**: PDFBox for core PDF operations, LibreOffice for conversions, PDF.js for client-side rendering
@@ -140,8 +140,6 @@ return useToolOperation({
- **API Controllers** (`src/main/java/.../controller/api/`): REST endpoints for PDF operations
- Organized by function: converters, security, misc, pipeline
- Follow pattern: `@RestController` + `@RequestMapping("/api/v1/...")`
- **Web Controllers** (`src/main/java/.../controller/web/`): Serve Thymeleaf templates
- Pattern: `@Controller` + return template names
### Key Components
- **SPDFApplication.java**: Main application class with desktop UI and browser launching logic
@@ -201,7 +199,6 @@ return useToolOperation({
- **Java Version**: Minimum JDK 17, supports and recommends JDK 21
- **Lombok**: Used extensively - ensure IDE plugin is installed
- **Desktop Mode**: Set `STIRLING_PDF_DESKTOP_UI=true` for desktop application mode
- **File Persistence**:
- **Backend**: Designed to be stateless - files are processed in memory/temp locations only
- **Frontend**: Uses IndexedDB for client-side file storage and caching (with thumbnails)
+3 -162
View File
@@ -2,7 +2,7 @@
## 1. Introduction
Stirling-PDF is a robust, locally hosted, web-based PDF manipulation tool. **Stirling 2.0** represents a complete frontend rewrite, replacing the legacy Thymeleaf-based UI with a modern React SPA (Single Page Application).
Stirling-PDF is a robust, locally hosted, web-based PDF manipulation tool. **Stirling 2.0** represents a complete frontend rewrite with a modern React SPA (Single Page Application).
This guide focuses on developing for Stirling 2.0, including both the React frontend and Spring Boot backend development workflows.
@@ -38,9 +38,6 @@ This guide focuses on developing for Stirling 2.0, including both the React fron
- PDF file association support
- Self-contained JRE bundling with JLink
**Legacy (reference only during development):**
- Thymeleaf templates (being completely replaced in 2.0)
## 3. Development Environment Setup
### Prerequisites
@@ -100,9 +97,6 @@ Stirling 2.0 uses client-side file storage:
- **PDF.js**: Handles client-side PDF rendering and processing
- **URL Parameters**: Support for deep linking and tool state persistence
### Legacy Code Reference
The existing Thymeleaf templates remain in the codebase during development as reference material but will be completely removed for the 2.0 release.
### Tauri Desktop App Development
Stirling-PDF can be packaged as a cross-platform desktop application using Tauri with PDF file association support and bundled JRE.
See [the frontend README](frontend/README.md#tauri) for build instructions.
@@ -154,7 +148,6 @@ Stirling-PDF/
│ │ │ ├── css/
│ │ │ ├── js/
│ │ │ └── pdfjs/
│ │ └── templates/ # Legacy Thymeleaf templates (reference only)
│ └── test/
├── testing/ # Cucumber and integration tests
│ └── cucumber/ # Cucumber test files
@@ -309,7 +302,6 @@ For quick iterations and development of Java backend, JavaScript, and UI compone
- RESTful API endpoints
- JavaScript functionality
- User interface components and styling
- Thymeleaf templates
To run Stirling-PDF locally:
@@ -401,7 +393,7 @@ Remember to test your changes thoroughly to ensure they don't break any existing
### React Component Development (Stirling 2.0)
For Stirling 2.0, new features are built as React components instead of Thymeleaf templates:
For Stirling 2.0, new features are built as React components:
#### Creating a New Tool Component
@@ -448,61 +440,6 @@ For Stirling 2.0, new features are built as React components instead of Thymelea
3. **Register in Tool Picker:**
Update the tool picker component to include the new tool with proper routing and URL parameter support.
### Legacy Reference: Overview of Thymeleaf
Thymeleaf is a server-side Java HTML template engine. It is used in Stirling-PDF to render dynamic web pages. Thymeleaf integrates heavily with Spring Boot.
### Thymeleaf overview
In Stirling-PDF, Thymeleaf is used to create HTML templates that are rendered on the server side. These templates are located in the `stirling-pdf/src/main/resources/templates` directory. Thymeleaf templates use a combination of HTML and special Thymeleaf attributes to dynamically generate content.
Some examples of this are:
```html
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
```
or
```html
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
```
Where it uses the `th:block`, `th:` indicating it's a special Thymeleaf element to be used server-side in generating the HTML, and block being the actual element type.
In this case, we are inserting the `navbar` entry within the `fragments/navbar.html` fragment into the `th:block` element.
They can be more complex, such as:
```html
<th:block th:insert="~{fragments/common :: head(title=#{pageExtracter.title}, header=#{pageExtracter.header})}"></th:block>
```
Which is the same as above but passes the parameters title and header into the fragment `common.html` to be used in its HTML generation.
Thymeleaf can also be used to loop through objects or pass things from the Java side into the HTML side.
```java
@GetMapping
public String newFeaturePage(Model model) {
model.addAttribute("exampleData", exampleData);
return "new-feature";
}
```
In the above example, if exampleData is a list of plain java objects of class Person and within it, you had id, name, age, etc. You can reference it like so
```html
<tbody>
<!-- Use th:each to iterate over the list -->
<tr th:each="person : ${exampleData}">
<td th:text="${person.id}"></td>
<td th:text="${person.name}"></td>
<td th:text="${person.age}"></td>
<td th:text="${person.email}"></td>
</tr>
</tbody>
```
This would generate n entries of tr for each person in exampleData
### Adding a New Feature to the Backend (API)
1. **Create a New Controller:**
@@ -527,7 +464,7 @@ This would generate n entries of tr for each person in exampleData
@GetMapping
@Operation(summary = "New Feature", description = "This is a new feature endpoint.")
public String newFeature() {
return "NewFeatureResponse"; // This refers to the NewFeatureResponse.html template presenting the user with the generated html from that file when they navigate to /api/v1/new-feature
return "NewFeatureResponse";
}
}
```
@@ -582,91 +519,6 @@ This would generate n entries of tr for each person in exampleData
}
```
### Adding a New Feature to the Frontend (UI)
1. **Create a New Thymeleaf Template:**
- Create a new HTML file in the `stirling-pdf/src/main/resources/templates` directory.
- Use Thymeleaf attributes to dynamically generate content.
- Use `extract-page.html` as a base example for the HTML template, which is useful to ensure importing of the general layout, navbar, and footer.
```html
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{newFeature.title}, header=#{newFeature.header})}"></th:block>
</head>
<body>
<div id="page-container">
<div id="content-wrap">
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<br><br>
<div class="container">
<div class="row justify-content-center">
<div class="col-md-6 bg-card">
<div class="tool-header">
<span class="material-symbols-rounded tool-header-icon organize">upload</span>
<span class="tool-header-text" th:text="#{newFeature.header}"></span>
</div>
<form th:action="@{'/api/v1/new-feature'}" method="post" enctype="multipart/form-data">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
<input type="hidden" id="customMode" name="customMode" value="">
<div class="mb-3">
<label for="featureInput" th:text="#{newFeature.prompt}"></label>
<input type="text" class="form-control" id="featureInput" name="featureInput" th:placeholder="#{newFeature.placeholder}" required>
</div>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{newFeature.submit}"></button>
</form>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>
```
2. **Create a New Controller for the UI:**
- Create a new Java class in the `stirling-pdf/src/main/java/stirling/software/SPDF/controller/ui` directory.
- Annotate the class with `@Controller` and `@RequestMapping` to define the UI endpoint.
```java
package stirling.software.SPDF.controller.ui;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import stirling.software.SPDF.service.NewFeatureService;
@Controller
@RequestMapping("/new-feature")
public class NewFeatureUIController {
@Autowired
private NewFeatureService newFeatureService;
@GetMapping
public String newFeaturePage(Model model) {
model.addAttribute("newFeatureData", newFeatureService.getNewFeatureData());
return "new-feature";
}
}
```
3. **Update the Navigation Bar:**
- Add a link to the new feature page in the navigation bar.
- Update the `stirling-pdf/src/main/resources/templates/fragments/navbar.html` file.
```html
<li class="nav-item">
<a class="nav-link" th:href="@{'/new-feature'}">New Feature</a>
</li>
```
## Adding New Translations to Existing Language Files in Stirling-PDF
When adding a new feature or modifying existing ones in Stirling-PDF, you'll need to add new translation entries to the existing language files. Here's a step-by-step guide:
@@ -696,15 +548,4 @@ pdfSplitter.input.pages=Enter page numbers to split
Add these entries to the default GB language file and any others you wish, translating the values as appropriate for each language.
### 3. Use Translations in Thymeleaf Templates
In your Thymeleaf templates, use the `#{key}` syntax to reference the new translations:
```html
<h1 th:text="#{pdfSplitter.title}">PDF Splitter</h1>
<p th:text="#{pdfSplitter.description}">Split your PDF into multiple documents</p>
<input type="text" th:placeholder="#{pdfSplitter.input.pages}">
<button th:text="#{pdfSplitter.button.split}">Split PDF</button>
```
Remember, never hard-code text in your templates or Java code. Always use translation keys to ensure proper localization.
+2 -3
View File
@@ -29,7 +29,6 @@ spotless {
dependencies {
api 'org.springframework.boot:spring-boot-starter-web'
api 'org.springframework.boot:spring-boot-starter-aop'
// api 'org.springframework.boot:spring-boot-starter-thymeleaf' // Deprecated - UI moved to React frontend
api 'com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20240325.1'
api 'com.fathzer:javaluator:3.0.6'
api 'com.posthog.java:posthog:1.2.0'
@@ -42,8 +41,8 @@ dependencies {
api "org.apache.pdfbox:preflight:$pdfboxVersion"
api 'com.github.junrar:junrar:7.5.7' // RAR archive support for CBR files
api 'jakarta.servlet:jakarta.servlet-api:6.1.0'
api 'org.snakeyaml:snakeyaml-engine:2.10'
api "org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.14"
api 'org.snakeyaml:snakeyaml-engine:3.0.1'
api "org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.15"
// Simple Java Mail for EML/MSG parsing (replaces direct Angus Mail usage)
api 'org.simplejavamail:simple-java-mail:8.12.6'
api 'org.simplejavamail:outlook-module:8.12.6' // MSG file support
@@ -303,22 +303,23 @@ public class EndpointConfiguration {
// Adding endpoints to "PageOps" group
addEndpointToGroup("PageOps", "remove-pages");
addEndpointToGroup("PageOps", "merge-pdfs");
addEndpointToGroup("PageOps", "split-pdfs");
addEndpointToGroup("PageOps", "pdf-organizer");
addEndpointToGroup("PageOps", "split-pages");
addEndpointToGroup("PageOps", "rearrange-pages");
addEndpointToGroup("PageOps", "rotate-pdf");
addEndpointToGroup("PageOps", "multi-page-layout");
addEndpointToGroup("PageOps", "booklet-imposition");
addEndpointToGroup("PageOps", "scale-pages");
addEndpointToGroup("PageOps", "crop");
addEndpointToGroup("PageOps", "extract-page");
addEndpointToGroup("PageOps", "pdf-to-single-page");
addEndpointToGroup("PageOps", "auto-split-pdf");
addEndpointToGroup("PageOps", "split-by-size-or-count");
addEndpointToGroup("PageOps", "overlay-pdf");
addEndpointToGroup("PageOps", "split-pdf-by-sections");
addEndpointToGroup("PageOps", "split-pdf-by-chapters");
addEndpointToGroup("PageOps", "add-page-numbers");
addEndpointToGroup("PageOps", "extract-pages");
// Adding endpoints to "Convert" group
// Adding endpoints to "Convert" group (Frontend has 15 convert endpoints)
addEndpointToGroup("Convert", "pdf-to-img");
addEndpointToGroup("Convert", "img-to-pdf");
addEndpointToGroup("Convert", "pdf-to-pdfa");
@@ -334,6 +335,8 @@ public class EndpointConfiguration {
addEndpointToGroup("Convert", "pdf-to-csv");
addEndpointToGroup("Convert", "pdf-to-markdown");
addEndpointToGroup("Convert", "eml-to-pdf");
addEndpointToGroup("Convert", "pdf-to-epub");
// Backend-only endpoints (not in frontend tool registry)
addEndpointToGroup("Convert", "pdf-to-vector");
addEndpointToGroup("Convert", "vector-to-pdf");
addEndpointToGroup("Convert", "pdf-to-video");
@@ -341,6 +344,7 @@ public class EndpointConfiguration {
addEndpointToGroup("Convert", "pdf-to-cbz");
addEndpointToGroup("Convert", "pdf-to-json");
addEndpointToGroup("Convert", "json-to-pdf");
addEndpointToGroup("Convert", "pdf-to-rtf");
// Adding endpoints to "Security" group
addEndpointToGroup("Security", "add-password");
@@ -351,51 +355,49 @@ public class EndpointConfiguration {
addEndpointToGroup("Security", "remove-cert-sign");
addEndpointToGroup("Security", "sanitize-pdf");
addEndpointToGroup("Security", "auto-redact");
addEndpointToGroup("Security", "redact");
addEndpointToGroup("Security", "validate-signature");
addEndpointToGroup("Security", "add-stamp");
addEndpointToGroup("Security", "unlock-pdf-forms");
// Backend-only endpoints (not in frontend tool registry endpoints)
addEndpointToGroup("Security", "redact");
addEndpointToGroup("Security", "verify-pdf");
addEndpointToGroup("Security", "stamp");
addEndpointToGroup("Security", "sign");
// Adding endpoints to "Other" group
addEndpointToGroup("Other", "ocr-pdf");
addEndpointToGroup("Other", "add-image");
addEndpointToGroup("Other", "extract-images");
addEndpointToGroup("Other", "change-metadata");
addEndpointToGroup("Other", "update-metadata");
addEndpointToGroup("Other", "flatten");
addEndpointToGroup("Other", "unlock-pdf-forms");
addEndpointToGroup("Other", REMOVE_BLANKS);
addEndpointToGroup("Other", "remove-annotations");
addEndpointToGroup("Other", "compare");
addEndpointToGroup("Other", "add-page-numbers");
addEndpointToGroup("Other", "get-info-on-pdf");
addEndpointToGroup("Other", "remove-image-pdf");
addEndpointToGroup("Other", "add-attachments");
addEndpointToGroup("Other", "replace-invert-pdf");
addEndpointToGroup("Other", "edit-table-of-contents");
addEndpointToGroup("Other", "text-editor-pdf");
// Backend-only endpoints (not in frontend tool registry endpoints)
addEndpointToGroup("Other", "add-image");
addEndpointToGroup("Other", "compare");
addEndpointToGroup("Other", "view-pdf");
addEndpointToGroup("Other", "replace-and-invert-color-pdf");
addEndpointToGroup("Other", "multi-tool");
// Adding form-related endpoints to "Other" group
addEndpointToGroup("Other", "fields");
addEndpointToGroup("Other", "modify-fields");
addEndpointToGroup("Other", "delete-fields");
addEndpointToGroup("Other", "fill");
// Adding endpoints to "Advance" group
addEndpointToGroup("Advance", "adjust-contrast");
addEndpointToGroup("Advance", "compress-pdf");
addEndpointToGroup("Advance", "extract-image-scans");
addEndpointToGroup("Advance", "repair");
addEndpointToGroup("Advance", "auto-rename");
addEndpointToGroup("Advance", "pipeline");
addEndpointToGroup("Advance", "handleData");
addEndpointToGroup("Advance", "scanner-effect");
addEndpointToGroup("Advance", "auto-split-pdf");
addEndpointToGroup("Advance", "show-javascript");
addEndpointToGroup("Advance", "split-by-size-or-count");
addEndpointToGroup("Advance", "overlay-pdf");
addEndpointToGroup("Advance", "split-pdf-by-sections");
addEndpointToGroup("Advance", "edit-table-of-contents");
addEndpointToGroup("Advance", "split-pdf-by-chapters");
// Backend-only endpoints
addEndpointToGroup("Advance", "adjust-contrast");
addEndpointToGroup("Advance", "pipeline");
// CLI
addEndpointToGroup("CLI", "compress-pdf");
@@ -436,8 +438,8 @@ public class EndpointConfiguration {
// Java
addEndpointToGroup("Java", "merge-pdfs");
addEndpointToGroup("Java", "remove-pages");
addEndpointToGroup("Java", "split-pdfs");
addEndpointToGroup("Java", "pdf-organizer");
addEndpointToGroup("Java", "split-pages");
addEndpointToGroup("Java", "rearrange-pages");
addEndpointToGroup("Java", "rotate-pdf");
addEndpointToGroup("Java", "pdf-to-img");
addEndpointToGroup("Java", "img-to-pdf");
@@ -445,9 +447,10 @@ public class EndpointConfiguration {
addEndpointToGroup("Java", "remove-password");
addEndpointToGroup("Java", "change-permissions");
addEndpointToGroup("Java", "add-watermark");
addEndpointToGroup("Java", "add-stamp");
addEndpointToGroup("Java", "add-image");
addEndpointToGroup("Java", "extract-images");
addEndpointToGroup("Java", "change-metadata");
addEndpointToGroup("Java", "update-metadata");
addEndpointToGroup("Java", "cert-sign");
addEndpointToGroup("Java", "remove-cert-sign");
addEndpointToGroup("Java", "multi-page-layout");
@@ -459,7 +462,6 @@ public class EndpointConfiguration {
addEndpointToGroup("Java", "sanitize-pdf");
addEndpointToGroup("Java", "crop");
addEndpointToGroup("Java", "get-info-on-pdf");
addEndpointToGroup("Java", "extract-page");
addEndpointToGroup("Java", "pdf-to-single-page");
addEndpointToGroup("Java", "markdown-to-pdf");
addEndpointToGroup("Java", "show-javascript");
@@ -469,7 +471,9 @@ public class EndpointConfiguration {
addEndpointToGroup("Java", "split-by-size-or-count");
addEndpointToGroup("Java", "overlay-pdf");
addEndpointToGroup("Java", "split-pdf-by-sections");
addEndpointToGroup("Java", "split-pdf-by-chapters");
addEndpointToGroup("Java", REMOVE_BLANKS);
addEndpointToGroup("Java", "remove-annotations");
addEndpointToGroup("Java", "pdf-to-text");
addEndpointToGroup("Java", "remove-image-pdf");
addEndpointToGroup("Java", "pdf-to-markdown");
@@ -479,15 +483,24 @@ public class EndpointConfiguration {
addEndpointToGroup("Java", "pdf-to-cbz");
addEndpointToGroup("Java", "pdf-to-json");
addEndpointToGroup("Java", "json-to-pdf");
addEndpointToGroup("rar", "pdf-to-cbr");
addEndpointToGroup("Java", "pdf-to-video");
addEndpointToGroup("Java", "verify-pdf");
addEndpointToGroup("Java", "flatten");
addEndpointToGroup("Java", "unlock-pdf-forms");
addEndpointToGroup("Java", "validate-signature");
addEndpointToGroup("Java", "text-editor-pdf");
addEndpointToGroup("Java", "edit-table-of-contents");
addEndpointToGroup("Java", "pdf-to-epub");
addEndpointToGroup("Java", "eml-to-pdf");
addEndpointToGroup("Java", "handleData");
addEndpointToGroup("rar", "pdf-to-cbr");
// Javascript
addEndpointToGroup("Javascript", "pdf-organizer");
addEndpointToGroup("Javascript", "rearrange-pages");
addEndpointToGroup("Javascript", "sign");
addEndpointToGroup("Javascript", "compare");
addEndpointToGroup("Javascript", "adjust-contrast");
addEndpointToGroup("Javascript", "text-editor-pdf");
/* qpdf */
addEndpointToGroup("qpdf", "repair");
@@ -498,6 +511,7 @@ public class EndpointConfiguration {
addEndpointToGroup("Ghostscript", "compress-pdf");
addEndpointToGroup("Ghostscript", "crop");
addEndpointToGroup("Ghostscript", "replace-invert-pdf");
addEndpointToGroup("Ghostscript", "scanner-effect");
addEndpointToGroup("Ghostscript", "pdf-to-vector");
addEndpointToGroup("Ghostscript", "vector-to-pdf");
@@ -516,6 +530,8 @@ public class EndpointConfiguration {
addEndpointAlternative("compress-pdf", "qpdf");
addEndpointAlternative("compress-pdf", "Ghostscript");
addEndpointAlternative("compress-pdf", "Java");
addEndpointAlternative("crop", "Ghostscript");
addEndpointAlternative("crop", "Java");
addEndpointAlternative("ocr-pdf", "tesseract");
addEndpointAlternative("ocr-pdf", "OCRmyPDF");
@@ -544,6 +560,9 @@ public class EndpointConfiguration {
// Pdftohtml dependent endpoints
addEndpointToGroup("Pdftohtml", "pdf-to-html");
addEndpointToGroup("Pdftohtml", "pdf-to-markdown");
// Calibre dependent endpoints
addEndpointToGroup("Calibre", "pdf-to-epub");
}
private void processEnvironmentConfigs() {
@@ -64,16 +64,6 @@ public class AppConfig {
return v2Enabled;
}
/* Commented out Thymeleaf template engine bean - to be removed when frontend migration is complete
@Bean
@ConditionalOnProperty(name = "system.customHTMLFiles", havingValue = "true")
public SpringTemplateEngine templateEngine(ResourceLoader resourceLoader) {
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
templateEngine.addTemplateResolver(new FileFallbackTemplateResolver(resourceLoader));
return templateEngine;
}
*/
@Bean(name = "loginEnabled")
public boolean loginEnabled() {
return applicationProperties.getSecurity().isEnableLogin();
@@ -1,49 +0,0 @@
package stirling.software.common.configuration;
/* Commented out entire FileFallbackTemplateResolver class - Thymeleaf dependency removed
* This class will be removed when frontend migration to React is complete
@Slf4j
public class FileFallbackTemplateResolver extends AbstractConfigurableTemplateResolver {
private final ResourceLoader resourceLoader;
public FileFallbackTemplateResolver(ResourceLoader resourceLoader) {
super();
this.resourceLoader = resourceLoader;
setSuffix(".html");
}
// Note this does not work in local IDE, Prod jar only.
@Override
protected ITemplateResource computeTemplateResource(
IEngineConfiguration configuration,
String ownerTemplate,
String template,
String resourceName,
String characterEncoding,
Map<String, Object> templateResolutionAttributes) {
Resource resource =
resourceLoader.getResource(
"file:" + InstallationPathConfig.getTemplatesPath() + resourceName);
try {
if (resource.exists() && resource.isReadable()) {
return new FileTemplateResource(resource.getFile().getPath(), characterEncoding);
}
} catch (IOException e) {
// Log the exception to help with debugging issues loading external templates
log.warn("Unable to read template '{}' from file system", resourceName, e);
}
InputStream inputStream =
Thread.currentThread()
.getContextClassLoader()
.getResourceAsStream("templates/" + resourceName);
if (inputStream != null) {
return new InputStreamTemplateResource(inputStream, "UTF-8");
}
return null;
}
}
*/
@@ -1,8 +1,6 @@
package stirling.software.common.configuration;
import java.io.File;
import java.nio.file.Paths;
import java.util.Locale;
import lombok.extern.slf4j.Slf4j;
@@ -61,28 +59,6 @@ public class InstallationPathConfig {
}
private static String initializeBasePath() {
if (Boolean.parseBoolean(System.getProperty("STIRLING_PDF_DESKTOP_UI", "false"))) {
String os = System.getProperty("os.name").toLowerCase(Locale.ROOT);
if (os.contains("win")) {
return Paths.get(
System.getenv("APPDATA"), // parent path
"Stirling-PDF")
+ File.separator;
} else if (os.contains("mac")) {
return Paths.get(
System.getProperty("user.home"),
"Library",
"Application Support",
"Stirling-PDF")
+ File.separator;
} else {
return Paths.get(
System.getProperty("user.home"), // parent path
".config",
"Stirling-PDF")
+ File.separator;
}
}
return "." + File.separator;
}
@@ -1,7 +1,14 @@
package stirling.software.common.configuration;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import org.apache.commons.lang3.StringUtils;
import org.springframework.context.annotation.Configuration;
@@ -14,6 +21,8 @@ import stirling.software.common.model.ApplicationProperties.CustomPaths;
import stirling.software.common.model.ApplicationProperties.CustomPaths.Operations;
import stirling.software.common.model.ApplicationProperties.CustomPaths.Pipeline;
import stirling.software.common.model.ApplicationProperties.System;
import stirling.software.common.util.ProcessExecutor;
import stirling.software.common.util.UnoServerPool;
@Slf4j
@Configuration
@@ -32,8 +41,11 @@ public class RuntimePathConfig {
// Tesseract data path
private final String tessDataPath;
private final List<ApplicationProperties.ProcessExecutor.UnoServerEndpoint> unoServerEndpoints;
// Pipeline paths
private final String pipelineWatchedFoldersPath;
private final List<String> pipelineWatchedFoldersPaths;
private final String pipelineFinishedFoldersPath;
private final String pipelineDefaultWebUiConfigs;
private final String pipelinePath;
@@ -42,20 +54,27 @@ public class RuntimePathConfig {
this.properties = properties;
this.basePath = InstallationPathConfig.getPath();
this.pipelinePath = Path.of(basePath, "pipeline").toString();
String defaultWatchedFolders = Path.of(this.pipelinePath, "watchedFolders").toString();
String defaultFinishedFolders = Path.of(this.pipelinePath, "finishedFolders").toString();
String defaultWebUIConfigs = Path.of(this.pipelinePath, "defaultWebUIConfigs").toString();
System system = properties.getSystem();
CustomPaths customPaths = system.getCustomPaths();
Pipeline pipeline = customPaths.getPipeline();
this.pipelineWatchedFoldersPath =
this.pipelinePath =
resolvePath(
Path.of(basePath, "pipeline").toString(),
pipeline != null ? pipeline.getPipelineDir() : null);
String defaultWatchedFolders = Path.of(this.pipelinePath, "watchedFolders").toString();
String defaultFinishedFolders = Path.of(this.pipelinePath, "finishedFolders").toString();
String defaultWebUIConfigs = Path.of(this.pipelinePath, "defaultWebUIConfigs").toString();
List<String> watchedFoldersDirs =
sanitizePathList(pipeline != null ? pipeline.getWatchedFoldersDirs() : null);
this.pipelineWatchedFoldersPaths =
resolveWatchedFolderPaths(
defaultWatchedFolders,
watchedFoldersDirs,
pipeline != null ? pipeline.getWatchedFoldersDir() : null);
this.pipelineWatchedFoldersPath = this.pipelineWatchedFoldersPaths.get(0);
this.pipelineFinishedFoldersPath =
resolvePath(
defaultFinishedFolders,
@@ -65,6 +84,9 @@ public class RuntimePathConfig {
defaultWebUIConfigs,
pipeline != null ? pipeline.getWebUIConfigsDir() : null);
// Validate path conflicts after all paths are resolved
validatePipelinePaths();
boolean isDocker = isRunningInDocker();
// Initialize Operation paths
@@ -108,13 +130,220 @@ public class RuntimePathConfig {
}
log.info("Using Tesseract data path: {}", this.tessDataPath);
ApplicationProperties.ProcessExecutor processExecutor = properties.getProcessExecutor();
int libreOfficeLimit = 1;
if (processExecutor != null && processExecutor.getSessionLimit() != null) {
libreOfficeLimit = processExecutor.getSessionLimit().getLibreOfficeSessionLimit();
}
this.unoServerEndpoints = buildUnoServerEndpoints(processExecutor, libreOfficeLimit);
ProcessExecutor.setUnoServerPool(new UnoServerPool(this.unoServerEndpoints));
}
private String resolvePath(String defaultPath, String customPath) {
return StringUtils.isNotBlank(customPath) ? customPath : defaultPath;
}
private List<String> resolveWatchedFolderPaths(
String defaultPath, List<String> watchedFoldersDirs, String legacyWatchedFolder) {
List<String> rawPaths = new ArrayList<>();
// Collect paths from new config
if (watchedFoldersDirs != null && !watchedFoldersDirs.isEmpty()) {
rawPaths.addAll(watchedFoldersDirs);
}
// Fall back to legacy config
else if (StringUtils.isNotBlank(legacyWatchedFolder)) {
rawPaths.add(legacyWatchedFolder);
}
// Fall back to default
else {
rawPaths.add(defaultPath);
}
// Validate, normalize, and deduplicate paths
List<String> validatedPaths = validateAndNormalizePaths(rawPaths);
// Ensure we have at least one valid path (critical for system to function)
if (validatedPaths.isEmpty()) {
log.warn(
"No valid watched folder paths configured, falling back to default: {}",
defaultPath);
validatedPaths.add(defaultPath);
}
// Detect overlapping paths (warning only, not blocking)
detectOverlappingPaths(validatedPaths);
return validatedPaths;
}
private List<String> sanitizePathList(List<String> paths) {
if (paths == null || paths.isEmpty()) {
return Collections.emptyList();
}
List<String> sanitized = new ArrayList<>();
for (String path : paths) {
if (StringUtils.isNotBlank(path)) {
sanitized.add(path.trim());
}
}
return sanitized;
}
private List<String> validateAndNormalizePaths(List<String> paths) {
Set<String> normalizedPaths = new LinkedHashSet<>(); // Preserves order, prevents duplicates
for (String pathStr : paths) {
if (StringUtils.isBlank(pathStr)) {
continue;
}
try {
// Normalize to absolute path
Path path = Paths.get(pathStr.trim()).toAbsolutePath().normalize();
String normalizedPath = path.toString();
// Check for duplicates
if (normalizedPaths.contains(normalizedPath)) {
log.debug("Skipping duplicate watched folder path: {}", pathStr);
continue;
}
normalizedPaths.add(normalizedPath);
log.info("Registered watched folder path: {}", normalizedPath);
} catch (InvalidPathException e) {
log.error(
"Invalid watched folder path '{}' - skipping: {}", pathStr, e.getMessage());
}
}
return new ArrayList<>(normalizedPaths);
}
private void detectOverlappingPaths(List<String> paths) {
for (int i = 0; i < paths.size(); i++) {
Path path1 = Paths.get(paths.get(i));
for (int j = i + 1; j < paths.size(); j++) {
Path path2 = Paths.get(paths.get(j));
// Check if one path is a parent of the other
if (path1.startsWith(path2)) {
log.warn(
"Watched folder path '{}' is nested inside '{}' - this may cause duplicate processing",
path1,
path2);
} else if (path2.startsWith(path1)) {
log.warn(
"Watched folder path '{}' is nested inside '{}' - this may cause duplicate processing",
path2,
path1);
}
}
}
}
private void validatePipelinePaths() {
try {
Path finishedPath = Paths.get(pipelineFinishedFoldersPath).toAbsolutePath().normalize();
for (String watchedPathStr : pipelineWatchedFoldersPaths) {
Path watchedPath = Paths.get(watchedPathStr).toAbsolutePath().normalize();
// Check if watched folder is same as finished folder
if (watchedPath.equals(finishedPath)) {
log.error(
"CRITICAL: Watched folder '{}' is the same as finished folder '{}' - this will cause processing loops!",
watchedPath,
finishedPath);
}
// Check if watched folder contains finished folder
else if (finishedPath.startsWith(watchedPath)) {
log.warn(
"Finished folder '{}' is nested inside watched folder '{}' - this may cause issues",
finishedPath,
watchedPath);
}
// Check if finished folder contains watched folder
else if (watchedPath.startsWith(finishedPath)) {
log.error(
"CRITICAL: Watched folder '{}' is nested inside finished folder '{}' - this will cause processing loops!",
watchedPath,
finishedPath);
}
}
} catch (Exception e) {
log.error("Error validating pipeline paths: {}", e.getMessage());
}
}
private boolean isRunningInDocker() {
return Files.exists(Path.of("/.dockerenv"));
}
private List<ApplicationProperties.ProcessExecutor.UnoServerEndpoint> buildUnoServerEndpoints(
ApplicationProperties.ProcessExecutor processExecutor, int sessionLimit) {
if (processExecutor == null) {
log.warn("ProcessExecutor config missing; defaulting to a single UNO endpoint.");
return Collections.singletonList(
new ApplicationProperties.ProcessExecutor.UnoServerEndpoint());
}
if (!processExecutor.isAutoUnoServer()) {
List<ApplicationProperties.ProcessExecutor.UnoServerEndpoint> configured =
sanitizeUnoServerEndpoints(processExecutor.getUnoServerEndpoints());
if (!configured.isEmpty()) {
// Warn if manual endpoint count doesn't match sessionLimit
if (configured.size() != sessionLimit) {
log.warn(
"Manual UNO endpoint count ({}) differs from libreOfficeSessionLimit ({}). "
+ "Concurrency will be limited by endpoint count, not sessionLimit.",
configured.size(),
sessionLimit);
}
return configured;
}
log.warn(
"autoUnoServer disabled but no unoServerEndpoints configured; defaulting to 127.0.0.1:2003.");
return Collections.singletonList(
new ApplicationProperties.ProcessExecutor.UnoServerEndpoint());
}
int count = sessionLimit > 0 ? sessionLimit : 1;
return buildAutoUnoServerEndpoints(count);
}
private List<ApplicationProperties.ProcessExecutor.UnoServerEndpoint>
buildAutoUnoServerEndpoints(int count) {
List<ApplicationProperties.ProcessExecutor.UnoServerEndpoint> endpoints = new ArrayList<>();
int basePort = 2003;
for (int i = 0; i < count; i++) {
ApplicationProperties.ProcessExecutor.UnoServerEndpoint endpoint =
new ApplicationProperties.ProcessExecutor.UnoServerEndpoint();
endpoint.setHost("127.0.0.1");
endpoint.setPort(basePort + (i * 2));
endpoints.add(endpoint);
}
return endpoints;
}
private List<ApplicationProperties.ProcessExecutor.UnoServerEndpoint>
sanitizeUnoServerEndpoints(
List<ApplicationProperties.ProcessExecutor.UnoServerEndpoint> endpoints) {
if (endpoints == null || endpoints.isEmpty()) {
return Collections.emptyList();
}
List<ApplicationProperties.ProcessExecutor.UnoServerEndpoint> sanitized = new ArrayList<>();
for (ApplicationProperties.ProcessExecutor.UnoServerEndpoint endpoint : endpoints) {
if (endpoint == null) {
continue;
}
String host = endpoint.getHost();
int port = endpoint.getPort();
if (host == null || host.isBlank() || port <= 0) {
continue;
}
sanitized.add(endpoint);
}
return sanitized;
}
}
@@ -164,6 +164,7 @@ public class ApplicationProperties {
private String customGlobalAPIKey;
private Jwt jwt = new Jwt();
private Validation validation = new Validation();
private String xFrameOptions = "DENY";
public Boolean isAltLogin() {
return saml2.getEnabled() || oauth2.getEnabled();
@@ -458,7 +459,9 @@ public class ApplicationProperties {
@Data
public static class Pipeline {
private String pipelineDir;
private String watchedFoldersDir;
private List<String> watchedFoldersDirs = new ArrayList<>();
private String finishedFoldersDir;
private String webUIConfigsDir;
}
@@ -827,6 +830,16 @@ public class ApplicationProperties {
public static class ProcessExecutor {
private SessionLimit sessionLimit = new SessionLimit();
private TimeoutMinutes timeoutMinutes = new TimeoutMinutes();
private boolean autoUnoServer = true;
private List<UnoServerEndpoint> unoServerEndpoints = new ArrayList<>();
@Data
public static class UnoServerEndpoint {
private String host = "127.0.0.1";
private int port = 2003;
private String hostLocation = "auto"; // auto|local|remote
private String protocol = "http"; // http|https
}
@Data
public static class SessionLimit {
@@ -1,40 +0,0 @@
package stirling.software.common.model;
/* Commented out entire InputStreamTemplateResource class - Thymeleaf dependency removed
* This class will be removed when frontend migration to React is complete
@RequiredArgsConstructor
@Getter
public class InputStreamTemplateResource implements ITemplateResource {
private final InputStream inputStream;
private final String characterEncoding;
@Override
public Reader reader() throws IOException {
return new InputStreamReader(inputStream, characterEncoding);
}
@Override
public ITemplateResource relative(String relativeLocation) {
// Implement logic for relative resources, if needed
throw new UnsupportedOperationException("Relative resources not supported");
}
@Override
public String getDescription() {
return "InputStream resource [Stream]";
}
@Override
public String getBaseName() {
return "streamResource";
}
@Override
public boolean exists() {
return inputStream != null;
}
}
*/
@@ -29,7 +29,7 @@ public class FileMonitor {
private final ConcurrentHashMap.KeySetView<Path, Boolean> readyForProcessingFiles;
private final WatchService watchService;
private final Predicate<Path> pathFilter;
private final Path rootDir;
private final List<Path> rootDirs;
private Set<Path> stagingFiles;
/**
@@ -47,8 +47,28 @@ public class FileMonitor {
this.pathFilter = pathFilter;
this.readyForProcessingFiles = ConcurrentHashMap.newKeySet();
this.watchService = FileSystems.getDefault().newWatchService();
log.info("Monitoring directory: {}", runtimePathConfig.getPipelineWatchedFoldersPath());
this.rootDir = Path.of(runtimePathConfig.getPipelineWatchedFoldersPath());
List<String> watchedFoldersDirs = runtimePathConfig.getPipelineWatchedFoldersPaths();
List<Path> validRootDirs = new ArrayList<>();
for (String pathStr : watchedFoldersDirs) {
try {
Path path = Path.of(pathStr);
validRootDirs.add(path);
log.info("Monitoring directory: {}", path);
} catch (Exception e) {
log.error(
"Failed to initialize monitoring for path '{}': {}",
pathStr,
e.getMessage());
}
}
this.rootDirs = Collections.unmodifiableList(validRootDirs);
if (this.rootDirs.isEmpty()) {
log.error("No valid directories to monitor - FileMonitor will not function");
}
}
private boolean shouldNotProcess(Path path) {
@@ -85,13 +105,15 @@ public class FileMonitor {
readyForProcessingFiles.clear();
if (path2KeyMapping.isEmpty()) {
log.warn("not monitoring any directory, even the root directory itself: {}", rootDir);
if (Files.exists(
rootDir)) { // if the root directory exists, re-register the root directory
try {
recursivelyRegisterEntry(rootDir);
} catch (IOException e) {
log.error("unable to register monitoring", e);
log.warn("Not monitoring any directories; attempting to re-register root paths.");
for (Path rootDir : rootDirs) {
if (Files.exists(
rootDir)) { // if the root directory exists, re-register the root directory
try {
recursivelyRegisterEntry(rootDir);
} catch (IOException e) {
log.error("unable to register monitoring for {}", rootDir, e);
}
}
}
}
@@ -4,6 +4,8 @@ import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
@@ -38,6 +40,10 @@ public class GeneralUtils {
*/
private static final int MAX_DNS_ADDRESSES = 20;
// Constants for size conversion
private static final BigDecimal KIB = BigDecimal.valueOf(1024L);
private static final BigDecimal LONG_MAX_DECIMAL = BigDecimal.valueOf(Long.MAX_VALUE);
private final Set<String> DEFAULT_VALID_SCRIPTS = Set.of("png_to_webp.py", "split_photos.py");
private final Set<String> DEFAULT_VALID_PIPELINE =
Set.of(
@@ -539,39 +545,26 @@ public class GeneralUtils {
try {
if (sizeStr.endsWith("TB")) {
return (long)
(Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2))
* 1024L
* 1024L
* 1024L
* 1024L);
return toBytes(parseSizeValue(sizeStr.substring(0, sizeStr.length() - 2)), 4);
} else if (sizeStr.endsWith("GB")) {
return (long)
(Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2))
* 1024L
* 1024L
* 1024L);
return toBytes(parseSizeValue(sizeStr.substring(0, sizeStr.length() - 2)), 3);
} else if (sizeStr.endsWith("MB")) {
return (long)
(Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2))
* 1024L
* 1024L);
return toBytes(parseSizeValue(sizeStr.substring(0, sizeStr.length() - 2)), 2);
} else if (sizeStr.endsWith("KB")) {
return (long)
(Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2)) * 1024L);
return toBytes(parseSizeValue(sizeStr.substring(0, sizeStr.length() - 2)), 1);
} else if (!sizeStr.isEmpty() && sizeStr.charAt(sizeStr.length() - 1) == 'B') {
return Long.parseLong(sizeStr.substring(0, sizeStr.length() - 1));
return toBytes(parseSizeValue(sizeStr.substring(0, sizeStr.length() - 1)), 0);
} else {
// Use provided default unit or fall back to MB
String unit = defaultUnit != null ? defaultUnit.toUpperCase(Locale.ROOT) : "MB";
double value = Double.parseDouble(sizeStr);
BigDecimal value = parseSizeValue(sizeStr);
return switch (unit) {
case "TB" -> (long) (value * 1024L * 1024L * 1024L * 1024L);
case "GB" -> (long) (value * 1024L * 1024L * 1024L);
case "MB" -> (long) (value * 1024L * 1024L);
case "KB" -> (long) (value * 1024L);
case "B" -> (long) value;
default -> (long) (value * 1024L * 1024L); // Default to MB
case "TB" -> toBytes(value, 4);
case "GB" -> toBytes(value, 3);
case "MB" -> toBytes(value, 2);
case "KB" -> toBytes(value, 1);
case "B" -> toBytes(value, 0);
default -> toBytes(value, 2); // Default to MB
};
}
} catch (NumberFormatException e) {
@@ -590,6 +583,30 @@ public class GeneralUtils {
return convertSizeToBytes(sizeStr, "MB");
}
private Long toBytes(BigDecimal value, int powerOf1024) {
if (value == null) {
return null;
}
if (value.compareTo(BigDecimal.ZERO) < 0) {
log.warn("Size value cannot be negative: {}", value);
return null;
}
if (powerOf1024 < 0 || powerOf1024 > 4) {
throw new IllegalArgumentException("Invalid power for size conversion: " + powerOf1024);
}
BigDecimal multiplier = powerOf1024 == 0 ? BigDecimal.ONE : KIB.pow(powerOf1024);
BigDecimal bytes = value.multiply(multiplier).setScale(0, RoundingMode.DOWN);
if (bytes.compareTo(LONG_MAX_DECIMAL) > 0) {
log.warn("Size value too large to fit in long: {}", bytes);
return null;
}
return bytes.longValue();
}
private BigDecimal parseSizeValue(String value) {
return new BigDecimal(value);
}
/* Validates if a string represents a valid size unit. */
private boolean isValidSizeUnit(String unit) {
// Use a precomputed Set for O(1) lookup, normalize using a locale-safe toUpperCase
@@ -360,8 +360,6 @@ public class PDFToFile {
Path inputFile, Path outputFile, String outputFormat, String libreOfficeFilter) {
List<String> command = new ArrayList<>();
command.add(runtimePathConfig.getUnoConvertPath());
command.add("--port");
command.add("2003");
command.add("--convert-to");
command.add(outputFormat);
if (libreOfficeFilter != null && !libreOfficeFilter.isBlank()) {
@@ -6,9 +6,12 @@ import java.io.IOException;
import java.io.InputStreamReader;
import java.io.InterruptedIOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
@@ -26,11 +29,15 @@ public class ProcessExecutor {
private static final Map<Processes, ProcessExecutor> instances = new ConcurrentHashMap<>();
private static ApplicationProperties applicationProperties = new ApplicationProperties();
private static volatile UnoServerPool unoServerPool;
private final Semaphore semaphore;
private final boolean liveUpdates;
private long timeoutDuration;
private final Processes processType;
private ProcessExecutor(int semaphoreLimit, boolean liveUpdates, long timeout) {
private ProcessExecutor(
Processes processType, int semaphoreLimit, boolean liveUpdates, long timeout) {
this.processType = processType;
this.semaphore = new Semaphore(semaphoreLimit);
this.liveUpdates = liveUpdates;
this.timeoutDuration = timeout;
@@ -173,10 +180,15 @@ public class ProcessExecutor {
.getTimeoutMinutes()
.getFfmpegTimeoutMinutes();
};
return new ProcessExecutor(semaphoreLimit, liveUpdates, timeoutMinutes);
return new ProcessExecutor(
processType, semaphoreLimit, liveUpdates, timeoutMinutes);
});
}
public static void setUnoServerPool(UnoServerPool pool) {
unoServerPool = pool;
}
public ProcessExecutorResult runCommandWithOutputHandling(List<String> command)
throws IOException, InterruptedException {
return runCommandWithOutputHandling(command, null);
@@ -186,11 +198,22 @@ public class ProcessExecutor {
List<String> command, File workingDirectory) throws IOException, InterruptedException {
String messages = "";
int exitCode = 1;
semaphore.acquire();
UnoServerPool.UnoServerLease unoLease = null;
boolean useSemaphore = true;
List<String> commandToRun = command;
if (shouldUseUnoServerPool(command)) {
unoLease = unoServerPool.acquireEndpoint();
commandToRun = applyUnoServerEndpoint(command, unoLease.getEndpoint());
useSemaphore = false;
}
if (useSemaphore) {
semaphore.acquire();
}
try {
log.info("Running command: {}", String.join(" ", command));
ProcessBuilder processBuilder = new ProcessBuilder(command);
validateCommand(commandToRun);
log.info("Running command: {}", String.join(" ", commandToRun));
ProcessBuilder processBuilder = new ProcessBuilder(commandToRun);
// Use the working directory if it's set
if (workingDirectory != null) {
@@ -268,7 +291,9 @@ public class ProcessExecutor {
outputReaderThread.join();
boolean isQpdf =
command != null && !command.isEmpty() && command.get(0).contains("qpdf");
commandToRun != null
&& !commandToRun.isEmpty()
&& commandToRun.get(0).contains("qpdf");
if (!outputLines.isEmpty()) {
String outputMessage = String.join("\n", outputLines);
@@ -309,11 +334,195 @@ public class ProcessExecutor {
}
}
} finally {
semaphore.release();
if (useSemaphore) {
semaphore.release();
}
if (unoLease != null) {
unoLease.close();
}
}
return new ProcessExecutorResult(exitCode, messages);
}
private boolean shouldUseUnoServerPool(List<String> command) {
if (processType != Processes.LIBRE_OFFICE || unoServerPool == null) {
return false;
}
if (unoServerPool.isEmpty()) {
return false;
}
if (command == null || command.isEmpty()) {
return false;
}
// Check if this is a UNO conversion by looking for unoconvert executable
String executable = command.get(0);
if (executable != null) {
// Extract basename from path for matching
String basename = executable;
int lastSlash = Math.max(executable.lastIndexOf('/'), executable.lastIndexOf('\\'));
if (lastSlash >= 0) {
basename = executable.substring(lastSlash + 1);
}
// Strip .exe extension on Windows
if (basename.toLowerCase(java.util.Locale.ROOT).endsWith(".exe")) {
basename = basename.substring(0, basename.length() - 4);
}
// Match common unoconvert variants (but NOT soffice)
String lowerBasename = basename.toLowerCase(java.util.Locale.ROOT);
if (lowerBasename.contains("unoconvert") || lowerBasename.equals("unoconv")) {
return true;
}
}
return false;
}
private List<String> applyUnoServerEndpoint(
List<String> command,
ApplicationProperties.ProcessExecutor.UnoServerEndpoint endpoint) {
if (endpoint == null || command == null || command.isEmpty()) {
return command;
}
List<String> updated = stripUnoEndpointArgs(command);
String host = endpoint.getHost();
int port = endpoint.getPort();
String hostLocation = endpoint.getHostLocation();
String protocol = endpoint.getProtocol();
// Normalize and validate host
if (host == null || host.isBlank()) {
host = "127.0.0.1";
}
// Normalize and validate port
if (port <= 0) {
port = 2003;
}
// Normalize and validate hostLocation (only auto|local|remote allowed)
if (hostLocation == null) {
hostLocation = "auto";
} else {
hostLocation = hostLocation.trim().toLowerCase(java.util.Locale.ROOT);
if (!Set.of("auto", "local", "remote").contains(hostLocation)) {
log.warn(
"Invalid hostLocation '{}' for endpoint {}:{}, defaulting to 'auto'",
hostLocation,
host,
port);
hostLocation = "auto";
}
}
// Normalize and validate protocol (only http|https allowed)
if (protocol == null) {
protocol = "http";
} else {
protocol = protocol.trim().toLowerCase(java.util.Locale.ROOT);
if (!Set.of("http", "https").contains(protocol)) {
log.warn(
"Invalid protocol '{}' for endpoint {}:{}, defaulting to 'http'",
protocol,
host,
port);
protocol = "http";
}
}
int insertIndex = Math.min(1, updated.size());
updated.add(insertIndex++, "--host");
updated.add(insertIndex++, host);
updated.add(insertIndex++, "--port");
updated.add(insertIndex++, String.valueOf(port));
// Only inject --host-location if non-default (for compatibility with older unoconvert)
if (!"auto".equals(hostLocation)) {
updated.add(insertIndex++, "--host-location");
updated.add(insertIndex++, hostLocation);
}
// Only inject --protocol if non-default (for compatibility with older unoconvert)
if (!"http".equals(protocol)) {
updated.add(insertIndex++, "--protocol");
updated.add(insertIndex, protocol);
}
return updated;
}
private List<String> stripUnoEndpointArgs(List<String> command) {
List<String> stripped = new ArrayList<>(command.size());
for (int i = 0; i < command.size(); i++) {
String arg = command.get(i);
if ("--host".equals(arg)
|| "--port".equals(arg)
|| "--host-location".equals(arg)
|| "--protocol".equals(arg)) {
i++;
continue;
}
if (arg != null
&& (arg.startsWith("--host=")
|| arg.startsWith("--port=")
|| arg.startsWith("--host-location=")
|| arg.startsWith("--protocol="))) {
continue;
}
stripped.add(arg);
}
return stripped;
}
private void validateCommand(List<String> command) {
if (command == null || command.isEmpty()) {
throw new IllegalArgumentException("Command must not be empty");
}
// Validate all arguments for null bytes and newlines (actual security concerns)
for (String arg : command) {
if (arg == null) {
throw new IllegalArgumentException("Command contains null argument");
}
if (arg.indexOf('\0') >= 0 || arg.indexOf('\n') >= 0 || arg.indexOf('\r') >= 0) {
throw new IllegalArgumentException("Command contains invalid characters");
}
}
// Validate executable (first argument)
String executable = command.get(0);
if (executable == null || executable.isBlank()) {
throw new IllegalArgumentException("Command executable must not be empty");
}
// Check for path traversal in executable
if (executable.contains("..")) {
throw new IllegalArgumentException(
"Command executable contains path traversal: " + executable);
}
// For absolute paths, verify the file exists and is executable
if (executable.contains("/") || executable.contains("\\")) {
Path execPath;
try {
execPath = Path.of(executable);
} catch (Exception e) {
throw new IllegalArgumentException("Invalid executable path: " + executable, e);
}
if (!Files.exists(execPath)) {
throw new IllegalArgumentException(
"Command executable does not exist: " + executable);
}
if (!Files.isRegularFile(execPath)) {
throw new IllegalArgumentException(
"Command executable is not a regular file: " + executable);
}
}
// For relative paths, trust that PATH resolution will work or fail appropriately
}
public enum Processes {
LIBRE_OFFICE,
PDFTOHTML,
@@ -38,12 +38,12 @@ 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")) {
if ("/robots.txt".equals(normalizedUri)
|| "/favicon.ico".equals(normalizedUri)
|| "/manifest.json".equals(normalizedUri)
|| "/site.webmanifest".equals(normalizedUri)
|| "/manifest-classic.json".equals(normalizedUri)
|| "/index.html".equals(normalizedUri)) {
return true;
}
@@ -173,6 +173,12 @@ public class RequestUriUtils {
"/api/v1/ui-data/footer-info") // Public footer configuration
|| trimmedUri.startsWith("/api/v1/invite/validate")
|| trimmedUri.startsWith("/api/v1/invite/accept")
// Health Endoints
|| trimmedUri.startsWith("/actuator/health")
|| trimmedUri.startsWith("/health")
|| trimmedUri.startsWith("/healthz")
|| trimmedUri.startsWith("/liveness")
|| trimmedUri.startsWith("/readiness")
|| trimmedUri.startsWith(
"/api/v1/mobile-scanner/") // Mobile scanner endpoints (no auth)
|| trimmedUri.startsWith("/v1/api-docs");
@@ -0,0 +1,280 @@
package stirling.software.common.util;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Pattern;
import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.springframework.stereotype.Component;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import stirling.software.common.model.ApplicationProperties;
import stirling.software.common.service.SsrfProtectionService;
@Component
@RequiredArgsConstructor
@Slf4j
public class SvgSanitizer {
private static final Set<String> DANGEROUS_ELEMENTS =
Set.of("script", "foreignobject", "iframe", "object", "embed", "handler", "listener");
private static final Set<String> URL_ATTRIBUTES = Set.of("href", "xlink:href", "src", "data");
private static final Pattern JAVASCRIPT_URL_PATTERN =
Pattern.compile("^\\s*javascript\\s*:", Pattern.CASE_INSENSITIVE);
private static final Pattern DATA_SCRIPT_PATTERN =
Pattern.compile(
"^\\s*data\\s*:[^,]*(?:script|javascript|vbscript)", Pattern.CASE_INSENSITIVE);
private final SsrfProtectionService ssrfProtectionService;
private final ApplicationProperties applicationProperties;
public byte[] sanitize(byte[] svgBytes) throws IOException {
if (svgBytes == null || svgBytes.length == 0) {
throw new IOException("SVG input is empty or null");
}
if (applicationProperties.getSystem().isDisableSanitize()) {
log.debug("SVG sanitization disabled by configuration");
return svgBytes;
}
try {
Document doc = parseSecurely(svgBytes);
Element root = doc.getDocumentElement();
if (root == null) {
throw new IOException("SVG document has no root element");
}
sanitizeNode(root);
byte[] result = serializeDocument(doc);
if (result == null || result.length == 0) {
throw new IOException("SVG sanitization produced empty output");
}
return result;
} catch (ParserConfigurationException | SAXException | TransformerException e) {
throw new IOException("Failed to sanitize SVG content", e);
}
}
private Document parseSecurely(byte[] svgBytes)
throws ParserConfigurationException, SAXException, IOException {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
factory.setXIncludeAware(false);
factory.setExpandEntityReferences(false);
factory.setNamespaceAware(true);
DocumentBuilder builder = factory.newDocumentBuilder();
return builder.parse(new ByteArrayInputStream(svgBytes));
}
private void sanitizeNode(Node node) {
if (node == null) {
return;
}
NodeList children = node.getChildNodes();
Set<Node> nodesToRemove = new HashSet<>();
for (int i = 0; i < children.getLength(); i++) {
Node child = children.item(i);
if (child.getNodeType() == Node.ELEMENT_NODE) {
String localName = child.getLocalName();
if (localName == null) {
localName = child.getNodeName();
}
if (isDangerousElement(localName)) {
log.warn("Removing dangerous SVG element: {}", localName);
nodesToRemove.add(child);
} else {
sanitizeNode(child);
}
}
}
for (Node toRemove : nodesToRemove) {
node.removeChild(toRemove);
}
if (node.getNodeType() == Node.ELEMENT_NODE) {
sanitizeAttributes((Element) node);
}
}
private void sanitizeAttributes(Element element) {
NamedNodeMap attributes = element.getAttributes();
Set<String> attributesToRemove = new HashSet<>();
for (int i = 0; i < attributes.getLength(); i++) {
Node attr = attributes.item(i);
String attrName = attr.getNodeName().toLowerCase();
String attrValue = attr.getNodeValue();
if (isEventHandler(attrName)) {
log.warn("Removing event handler attribute: {}", attrName);
attributesToRemove.add(attr.getNodeName());
continue;
}
if (isUrlAttribute(attrName)) {
if (isDangerousUrl(attrValue)) {
log.warn(
"Removing dangerous URL in attribute {}: {}",
attrName,
truncateForLog(attrValue));
attributesToRemove.add(attr.getNodeName());
continue;
}
if (isExternalUrl(attrValue) && !isUrlAllowed(attrValue)) {
log.warn(
"Removing SSRF-blocked URL in attribute {}: {}",
attrName,
truncateForLog(attrValue));
attributesToRemove.add(attr.getNodeName());
}
}
}
for (String attrName : attributesToRemove) {
element.removeAttribute(attrName);
}
}
private boolean isDangerousElement(String localName) {
return DANGEROUS_ELEMENTS.contains(localName.toLowerCase());
}
private boolean isEventHandler(String attrName) {
return attrName.startsWith("on");
}
private boolean isUrlAttribute(String attrName) {
return URL_ATTRIBUTES.contains(attrName.toLowerCase())
|| attrName.toLowerCase().endsWith(":href");
}
private boolean isDangerousUrl(String url) {
if (url == null || url.trim().isEmpty()) {
return false;
}
String normalized = normalizeUrl(url);
if (JAVASCRIPT_URL_PATTERN.matcher(normalized).find()) {
return true;
}
if (DATA_SCRIPT_PATTERN.matcher(normalized).find()) {
return true;
}
return false;
}
private String normalizeUrl(String url) {
if (url == null) {
return "";
}
String result = url.trim();
result = result.replaceAll("\u0000", "");
for (int i = 0; i < 3; i++) {
try {
String decoded = URLDecoder.decode(result, StandardCharsets.UTF_8);
if (decoded.equals(result)) {
break; // No more decoding needed
}
result = decoded;
} catch (Exception e) {
log.debug("Failed to decode URL, continuing with current value", e);
break;
}
}
return result.toLowerCase();
}
private boolean isExternalUrl(String url) {
if (url == null || url.trim().isEmpty()) {
return false;
}
String normalized = normalizeUrl(url);
if (normalized.startsWith("#")) {
return false;
}
if (normalized.startsWith("data:")) {
return false;
}
return normalized.startsWith("http://")
|| normalized.startsWith("https://")
|| normalized.startsWith("//")
|| normalized.startsWith("file:");
}
private boolean isUrlAllowed(String url) {
if (ssrfProtectionService == null) {
return true;
}
return ssrfProtectionService.isUrlAllowed(url);
}
private byte[] serializeDocument(Document doc) throws TransformerException {
TransformerFactory transformerFactory = TransformerFactory.newInstance();
transformerFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
Transformer transformer = transformerFactory.newTransformer();
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
transformer.setOutputProperty(OutputKeys.INDENT, "no");
transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
transformer.transform(new DOMSource(doc), new StreamResult(outputStream));
return outputStream.toByteArray();
}
private String truncateForLog(String value) {
if (value == null) {
return "null";
}
return value.length() > 50 ? value.substring(0, 50) + "..." : value;
}
}
@@ -0,0 +1,85 @@
package stirling.software.common.util;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import stirling.software.common.model.ApplicationProperties;
public class UnoServerPool {
private final List<ApplicationProperties.ProcessExecutor.UnoServerEndpoint> endpoints;
private final BlockingQueue<Integer> availableIndices;
public UnoServerPool(List<ApplicationProperties.ProcessExecutor.UnoServerEndpoint> endpoints) {
if (endpoints == null || endpoints.isEmpty()) {
this.endpoints = Collections.emptyList();
this.availableIndices = new LinkedBlockingQueue<>();
} else {
this.endpoints = new ArrayList<>(endpoints);
this.availableIndices = new LinkedBlockingQueue<>();
// Initialize queue with all endpoint indices
for (int i = 0; i < this.endpoints.size(); i++) {
this.availableIndices.offer(i);
}
}
}
public boolean isEmpty() {
return endpoints.isEmpty();
}
public UnoServerLease acquireEndpoint() throws InterruptedException {
if (endpoints.isEmpty()) {
return new UnoServerLease(defaultEndpoint(), null, this);
}
// Block until an endpoint index becomes available
Integer index = availableIndices.take();
return new UnoServerLease(endpoints.get(index), index, this);
}
private void releaseEndpoint(Integer index) {
if (index != null) {
availableIndices.offer(index);
}
}
private static ApplicationProperties.ProcessExecutor.UnoServerEndpoint defaultEndpoint() {
return new ApplicationProperties.ProcessExecutor.UnoServerEndpoint();
}
public static class UnoServerLease implements AutoCloseable {
private final ApplicationProperties.ProcessExecutor.UnoServerEndpoint endpoint;
private final Integer index;
private final UnoServerPool pool;
private final AtomicBoolean closed = new AtomicBoolean(false);
public UnoServerLease(
ApplicationProperties.ProcessExecutor.UnoServerEndpoint endpoint,
Integer index,
UnoServerPool pool) {
this.endpoint = endpoint;
this.index = index;
this.pool = pool;
}
public ApplicationProperties.ProcessExecutor.UnoServerEndpoint getEndpoint() {
return endpoint;
}
@Override
public void close() {
// Idempotent close: only release once even if close() called multiple times
if (!closed.compareAndSet(false, true)) {
return;
}
if (pool != null && index != null) {
pool.releaseEndpoint(index);
}
}
}
}
@@ -1,88 +0,0 @@
package stirling.software.common.model;
/* Commented out - InputStreamTemplateResource class removed with Thymeleaf migration
* This test will be removed when frontend migration to React is complete
public class InputStreamTemplateResourceTest {
@Test
void gettersReturnProvidedFields() {
byte[] data = {1, 2, 3};
InputStream is = new ByteArrayInputStream(data);
String encoding = "UTF-8";
InputStreamTemplateResource resource = new InputStreamTemplateResource(is, encoding);
assertSame(is, resource.getInputStream());
assertEquals(encoding, resource.getCharacterEncoding());
}
@Test
void fieldsAreFinal() throws NoSuchFieldException {
Field inputStreamField = InputStreamTemplateResource.class.getDeclaredField("inputStream");
Field characterEncodingField =
InputStreamTemplateResource.class.getDeclaredField("characterEncoding");
assertTrue(Modifier.isFinal(inputStreamField.getModifiers()));
assertTrue(Modifier.isFinal(characterEncodingField.getModifiers()));
}
@Test
void noSetterMethodsPresent() {
long setterCount =
Arrays.stream(InputStreamTemplateResource.class.getDeclaredMethods())
.filter(method -> method.getName().startsWith("set"))
.count();
assertEquals(0, setterCount, "InputStreamTemplateResource should not have setter methods");
}
@Test
void readerReturnsCorrectContent() throws Exception {
String content = "Hello, world!";
InputStream is = new ByteArrayInputStream(content.getBytes("UTF-8"));
InputStreamTemplateResource resource = new InputStreamTemplateResource(is, "UTF-8");
try (Reader reader = resource.reader()) {
char[] buffer = new char[content.length()];
int read = reader.read(buffer);
assertEquals(content.length(), read);
assertEquals(content, new String(buffer));
}
}
@Test
void relativeThrowsUnsupportedOperationException() {
InputStream is = new ByteArrayInputStream(new byte[0]);
InputStreamTemplateResource resource = new InputStreamTemplateResource(is, "UTF-8");
assertThrows(UnsupportedOperationException.class, () -> resource.relative("other"));
}
@Test
void getDescriptionReturnsExpectedString() {
InputStream is = new ByteArrayInputStream(new byte[0]);
InputStreamTemplateResource resource = new InputStreamTemplateResource(is, "UTF-8");
assertEquals("InputStream resource [Stream]", resource.getDescription());
}
@Test
void getBaseNameReturnsExpectedString() {
InputStream is = new ByteArrayInputStream(new byte[0]);
InputStreamTemplateResource resource = new InputStreamTemplateResource(is, "UTF-8");
assertEquals("streamResource", resource.getBaseName());
}
@Test
void existsReturnsTrueWhenInputStreamNotNull() {
InputStream is = new ByteArrayInputStream(new byte[0]);
InputStreamTemplateResource resource = new InputStreamTemplateResource(is, "UTF-8");
assertTrue(resource.exists());
}
@Test
void existsReturnsFalseWhenInputStreamIsNull() {
InputStreamTemplateResource resource = new InputStreamTemplateResource(null, "UTF-8");
assertFalse(resource.exists());
}
}
*/
@@ -9,6 +9,7 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.FileTime;
import java.time.Instant;
import java.util.List;
import java.util.function.Predicate;
import org.junit.jupiter.api.BeforeEach;
@@ -34,7 +35,8 @@ class FileMonitorTest {
@BeforeEach
void setUp() throws IOException {
when(runtimePathConfig.getPipelineWatchedFoldersPath()).thenReturn(tempDir.toString());
when(runtimePathConfig.getPipelineWatchedFoldersPaths())
.thenReturn(List.of(tempDir.toString()));
// This mock is used in all tests except testPathFilter
// We use lenient to avoid UnnecessaryStubbingException in that test
@@ -16,6 +16,14 @@ class GeneralUtilsAdditionalTest {
assertNull(GeneralUtils.convertSizeToBytes(null));
}
@Test
void testConvertSizeToBytesEdgeCases() {
assertNull(GeneralUtils.convertSizeToBytes("-10MB"));
assertNull(GeneralUtils.convertSizeToBytes("10000000TB")); // overflow beyond long
assertEquals(1099511627776L, GeneralUtils.convertSizeToBytes("1TB"));
assertEquals(2684354560L, GeneralUtils.convertSizeToBytes("2.5GB"));
}
@Test
void testFormatBytes() {
assertEquals("512 B", GeneralUtils.formatBytes(512));
@@ -37,21 +37,50 @@ public class ProcessExecutorTest {
@Test
public void testRunCommandWithOutputHandling_Error() {
// Mock the command to execute
// Test with a command that will fail to execute (non-existent command)
List<String> command = new ArrayList<>();
command.add("nonexistent-command");
command.add("nonexistent-command-that-does-not-exist");
// Execute the command and expect an IOException
IOException thrown =
// Execute the command and expect an IOException (command not found)
assertThrows(
IOException.class, () -> processExecutor.runCommandWithOutputHandling(command));
}
@Test
public void testRunCommandWithOutputHandling_PathTraversal() {
// Test that path traversal is blocked
List<String> command = new ArrayList<>();
command.add("../../../etc/passwd");
// Execute the command and expect an IllegalArgumentException
IllegalArgumentException thrown =
assertThrows(
IOException.class,
IllegalArgumentException.class,
() -> processExecutor.runCommandWithOutputHandling(command));
// Check the exception message to ensure it indicates the command was not found
// Check the exception message
String errorMessage = thrown.getMessage();
assertTrue(
errorMessage.contains("error=2")
|| errorMessage.contains("No such file or directory"),
errorMessage.contains("path traversal"),
"Unexpected error message: " + errorMessage);
}
@Test
public void testRunCommandWithOutputHandling_NullByte() {
// Test that null bytes are blocked
List<String> command = new ArrayList<>();
command.add("test\0command");
// Execute the command and expect an IllegalArgumentException
IllegalArgumentException thrown =
assertThrows(
IllegalArgumentException.class,
() -> processExecutor.runCommandWithOutputHandling(command));
// Check the exception message
String errorMessage = thrown.getMessage();
assertTrue(
errorMessage.contains("invalid characters"),
"Unexpected error message: " + errorMessage);
}
}
@@ -0,0 +1,207 @@
package stirling.software.common.util;
import static org.junit.jupiter.api.Assertions.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.jupiter.api.Test;
import stirling.software.common.model.ApplicationProperties;
public class UnoServerPoolTest {
@Test
void testEmptyPool() throws InterruptedException {
UnoServerPool pool = new UnoServerPool(Collections.emptyList());
assertTrue(pool.isEmpty(), "Pool with empty list should be empty");
UnoServerPool.UnoServerLease lease = pool.acquireEndpoint();
assertNotNull(lease, "Should return a default lease for empty pool");
assertNotNull(lease.getEndpoint(), "Default lease should have an endpoint");
lease.close(); // Should not throw
}
@Test
void testSingleEndpointAcquireRelease() throws InterruptedException {
List<ApplicationProperties.ProcessExecutor.UnoServerEndpoint> endpoints =
createEndpoints(1);
UnoServerPool pool = new UnoServerPool(endpoints);
assertFalse(pool.isEmpty(), "Pool should not be empty");
UnoServerPool.UnoServerLease lease = pool.acquireEndpoint();
assertNotNull(lease, "Should acquire endpoint");
assertEquals("127.0.0.1", lease.getEndpoint().getHost());
assertEquals(2003, lease.getEndpoint().getPort());
lease.close();
}
@Test
void testMultipleEndpointsDistribution() throws InterruptedException {
List<ApplicationProperties.ProcessExecutor.UnoServerEndpoint> endpoints =
createEndpoints(3);
UnoServerPool pool = new UnoServerPool(endpoints);
List<Integer> portsUsed = new ArrayList<>();
// Acquire all endpoints
try (UnoServerPool.UnoServerLease lease1 = pool.acquireEndpoint();
UnoServerPool.UnoServerLease lease2 = pool.acquireEndpoint();
UnoServerPool.UnoServerLease lease3 = pool.acquireEndpoint()) {
portsUsed.add(lease1.getEndpoint().getPort());
portsUsed.add(lease2.getEndpoint().getPort());
portsUsed.add(lease3.getEndpoint().getPort());
// All three endpoints should be in use (different ports)
assertEquals(3, portsUsed.stream().distinct().count(), "Should use all 3 endpoints");
}
// All released after try-with-resources
}
@Test
void testConcurrentAccess() throws InterruptedException {
int endpointCount = 3;
int threadCount = 10;
List<ApplicationProperties.ProcessExecutor.UnoServerEndpoint> endpoints =
createEndpoints(endpointCount);
UnoServerPool pool = new UnoServerPool(endpoints);
ExecutorService executor = Executors.newFixedThreadPool(threadCount);
CountDownLatch startLatch = new CountDownLatch(1);
CountDownLatch doneLatch = new CountDownLatch(threadCount);
AtomicInteger successCount = new AtomicInteger(0);
for (int i = 0; i < threadCount; i++) {
executor.submit(
() -> {
try {
startLatch.await(); // Wait for all threads to be ready
UnoServerPool.UnoServerLease lease = pool.acquireEndpoint();
assertNotNull(lease, "Should acquire endpoint");
Thread.sleep(10); // Simulate work
lease.close();
successCount.incrementAndGet();
} catch (Exception e) {
fail("Thread failed: " + e.getMessage());
} finally {
doneLatch.countDown();
}
});
}
startLatch.countDown(); // Start all threads
boolean finished = doneLatch.await(5, TimeUnit.SECONDS);
executor.shutdown();
assertTrue(finished, "All threads should complete within timeout");
assertEquals(
threadCount, successCount.get(), "All threads should successfully acquire/release");
}
@Test
void testBlockingBehavior() throws InterruptedException {
List<ApplicationProperties.ProcessExecutor.UnoServerEndpoint> endpoints =
createEndpoints(2);
UnoServerPool pool = new UnoServerPool(endpoints);
// Acquire both endpoints
UnoServerPool.UnoServerLease lease1 = pool.acquireEndpoint();
UnoServerPool.UnoServerLease lease2 = pool.acquireEndpoint();
AtomicInteger acquired = new AtomicInteger(0);
CountDownLatch acquireLatch = new CountDownLatch(1);
// Try to acquire a third endpoint in separate thread (should block)
Thread blockingThread =
new Thread(
() -> {
try {
acquireLatch.countDown(); // Signal we're about to block
UnoServerPool.UnoServerLease lease3 = pool.acquireEndpoint();
acquired.incrementAndGet();
lease3.close();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
blockingThread.start();
acquireLatch.await(); // Wait for thread to start
Thread.sleep(100); // Give it time to block
// Should still be 0 because thread is blocked
assertEquals(0, acquired.get(), "Third acquire should be blocked");
// Release one endpoint
lease1.close();
Thread.sleep(100); // Give blocked thread time to acquire
// Now the third acquire should succeed
assertEquals(1, acquired.get(), "Third acquire should succeed after release");
lease2.close();
blockingThread.join(1000);
assertFalse(blockingThread.isAlive(), "Thread should complete");
}
@Test
void testEndpointReuse() throws InterruptedException {
List<ApplicationProperties.ProcessExecutor.UnoServerEndpoint> endpoints =
createEndpoints(1);
UnoServerPool pool = new UnoServerPool(endpoints);
int port1, port2;
try (UnoServerPool.UnoServerLease lease1 = pool.acquireEndpoint()) {
port1 = lease1.getEndpoint().getPort();
}
try (UnoServerPool.UnoServerLease lease2 = pool.acquireEndpoint()) {
port2 = lease2.getEndpoint().getPort();
}
assertEquals(port1, port2, "Should reuse the same endpoint after release");
}
@Test
void testHostLocationAndProtocol() throws InterruptedException {
List<ApplicationProperties.ProcessExecutor.UnoServerEndpoint> endpoints = new ArrayList<>();
ApplicationProperties.ProcessExecutor.UnoServerEndpoint endpoint =
new ApplicationProperties.ProcessExecutor.UnoServerEndpoint();
endpoint.setHost("remote.server");
endpoint.setPort(8080);
endpoint.setHostLocation("remote");
endpoint.setProtocol("https");
endpoints.add(endpoint);
UnoServerPool pool = new UnoServerPool(endpoints);
try (UnoServerPool.UnoServerLease lease = pool.acquireEndpoint()) {
assertEquals("remote.server", lease.getEndpoint().getHost());
assertEquals(8080, lease.getEndpoint().getPort());
assertEquals("remote", lease.getEndpoint().getHostLocation());
assertEquals("https", lease.getEndpoint().getProtocol());
}
}
private List<ApplicationProperties.ProcessExecutor.UnoServerEndpoint> createEndpoints(
int count) {
List<ApplicationProperties.ProcessExecutor.UnoServerEndpoint> endpoints = new ArrayList<>();
for (int i = 0; i < count; i++) {
ApplicationProperties.ProcessExecutor.UnoServerEndpoint endpoint =
new ApplicationProperties.ProcessExecutor.UnoServerEndpoint();
endpoint.setHost("127.0.0.1");
endpoint.setPort(2003 + (i * 2));
endpoints.add(endpoint);
}
return endpoints;
}
}
+5 -24
View File
@@ -2,11 +2,6 @@ apply plugin: 'org.springframework.boot'
import org.apache.tools.ant.taskdefs.condition.Os
repositories {
maven { url = 'https://build.shibboleth.net/maven/releases' }
maven { url = 'https://maven.pkg.github.com/jcefmaven/jcefmaven' }
}
configurations {
developmentOnly
runtimeClasspath {
@@ -43,13 +38,6 @@ spotless {
}
dependencies {
if (System.getenv('STIRLING_PDF_DESKTOP_UI') != 'false'
|| (project.hasProperty('STIRLING_PDF_DESKTOP_UI')
&& project.getProperty('STIRLING_PDF_DESKTOP_UI') != 'false')) {
implementation 'org.openjfx:javafx-controls:21'
implementation 'org.openjfx:javafx-swing:21'
}
if (System.getenv('DISABLE_ADDITIONAL_FEATURES') != 'true'
|| (project.hasProperty('DISABLE_ADDITIONAL_FEATURES')
&& System.getProperty('DISABLE_ADDITIONAL_FEATURES') != 'true')) {
@@ -63,7 +51,7 @@ dependencies {
implementation 'commons-io:commons-io:2.21.0'
implementation "org.bouncycastle:bcprov-jdk18on:$bouncycastleVersion"
implementation "org.bouncycastle:bcpkix-jdk18on:$bouncycastleVersion"
implementation 'io.micrometer:micrometer-core:1.16.1'
implementation 'io.micrometer:micrometer-core:1.16.2'
implementation 'com.google.zxing:core:3.5.4'
implementation "org.commonmark:commonmark:$commonmarkVersion" // https://mvnrepository.com/artifact/org.commonmark/commonmark
implementation "org.commonmark:commonmark-ext-gfm-tables:$commonmarkVersion"
@@ -91,6 +79,9 @@ dependencies {
// Batik
implementation 'org.apache.xmlgraphics:batik-all:1.19'
// PDFBox Graphics2D bridge for Batik SVG to PDF conversion
implementation 'de.rototor.pdfbox:graphics2d:3.0.5'
// TwelveMonkeys
runtimeOnly "com.twelvemonkeys.imageio:imageio-batik:$imageioVersion"
runtimeOnly "com.twelvemonkeys.imageio:imageio-bmp:$imageioVersion"
@@ -117,19 +108,8 @@ sourceSets {
resources {
srcDirs += ['../configs']
}
java {
if (System.getenv('STIRLING_PDF_DESKTOP_UI') == 'false') {
exclude 'stirling/software/SPDF/UI/impl/**'
}
}
}
test {
java {
if (System.getenv('STIRLING_PDF_DESKTOP_UI') == 'false') {
exclude 'stirling/software/SPDF/UI/impl/**'
}
}
}
}
@@ -241,6 +221,7 @@ tasks.register('npmBuild', Exec) {
commandLine = Os.isFamily(Os.FAMILY_WINDOWS) ? ['cmd', '/c', 'npm', 'run', 'build'] : ['npm', 'run', 'build']
dependsOn npmInstall
inputs.dir(new File(frontendDir, 'src'))
inputs.dir(new File(frontendDir, 'public'))
inputs.file(new File(frontendDir, 'package.json'))
outputs.dir(frontendDistDir)
@@ -20,7 +20,6 @@ import org.springframework.scheduling.annotation.EnableScheduling;
import io.github.pixee.security.SystemCommand;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.extern.slf4j.Slf4j;
@@ -28,7 +27,6 @@ import stirling.software.common.configuration.AppConfig;
import stirling.software.common.configuration.ConfigInitializer;
import stirling.software.common.configuration.InstallationPathConfig;
import stirling.software.common.model.ApplicationProperties;
import stirling.software.common.util.UrlUtils;
@Slf4j
@EnableScheduling
@@ -60,19 +58,6 @@ public class SPDFApplication {
Properties props = new Properties();
if (Boolean.parseBoolean(System.getProperty("STIRLING_PDF_DESKTOP_UI", "false"))) {
System.setProperty("java.awt.headless", "false");
app.setHeadless(false);
props.put("java.awt.headless", "false");
props.put("spring.main.web-application-type", "servlet");
int desiredPort = 8080;
String port = UrlUtils.findAvailablePort(desiredPort);
props.put("server.port", port);
System.setProperty("server.port", port);
log.info("Desktop UI mode: Using port {}", port);
}
app.setAdditionalProfiles(getActiveProfile(args));
ConfigInitializer initializer = new ConfigInitializer();
@@ -153,13 +138,6 @@ public class SPDFApplication {
"Running in Tauri mode. Parent process PID: {}",
parentPid != null ? parentPid : "not set");
}
// Desktop UI initialization removed - webBrowser dependency eliminated
// Keep backwards compatibility for STIRLING_PDF_DESKTOP_UI system property
if (Boolean.parseBoolean(System.getProperty("STIRLING_PDF_DESKTOP_UI", "false"))) {
log.info("Desktop UI mode enabled, but WebBrowser functionality has been removed");
// webBrowser.initWebUI(url); // Removed - desktop UI eliminated
}
// Standard browser opening logic
String browserOpenEnv = env.getProperty("BROWSER_OPEN");
boolean browserOpen = browserOpenEnv != null && "true".equalsIgnoreCase(browserOpenEnv);
@@ -192,14 +170,6 @@ public class SPDFApplication {
}
}
@PreDestroy
public void cleanup() {
// webBrowser cleanup removed - desktop UI eliminated
// if (webBrowser != null) {
// webBrowser.cleanup();
// }
}
@EventListener
public void onWebServerInitialized(WebServerInitializedEvent event) {
int actualPort = event.getWebServer().getPort();
@@ -118,7 +118,8 @@ public class ExternalAppDepConfig {
for (String group : affectedGroups) {
List<String> affectedFeatures = getAffectedFeatures(group);
endpointConfiguration.disableGroup(group);
endpointConfiguration.disableGroup(
group, EndpointConfiguration.DisableReason.DEPENDENCY);
log.warn(
"Missing dependency: {} - Disabling group: {} (Affected features: {})",
command,
@@ -143,7 +144,8 @@ public class ExternalAppDepConfig {
commandToGroupMapping.getOrDefault(
command, List.of("Weasyprint"));
for (String group : affectedGroups) {
endpointConfiguration.disableGroup(group);
endpointConfiguration.disableGroup(
group, EndpointConfiguration.DisableReason.DEPENDENCY);
}
log.warn(
"WeasyPrint version {} is below required {} - disabling"
@@ -172,7 +174,8 @@ public class ExternalAppDepConfig {
List<String> affectedGroups =
commandToGroupMapping.getOrDefault(command, List.of("qpdf"));
for (String group : affectedGroups) {
endpointConfiguration.disableGroup(group);
endpointConfiguration.disableGroup(
group, EndpointConfiguration.DisableReason.DEPENDENCY);
}
log.warn(
"qpdf version {} is below required {} - disabling group(s): {}",
@@ -226,7 +229,8 @@ public class ExternalAppDepConfig {
int ec = runAndWait(List.of(python, "-c", "import cv2"), DEFAULT_TIMEOUT).exitCode();
if (ec != 0) {
List<String> openCVFeatures = getAffectedFeatures("OpenCV");
endpointConfiguration.disableGroup("OpenCV");
endpointConfiguration.disableGroup(
"OpenCV", EndpointConfiguration.DisableReason.DEPENDENCY);
log.warn(
"OpenCV not available in Python - Disabling OpenCV features: {}",
String.join(", ", openCVFeatures));
@@ -236,8 +240,10 @@ public class ExternalAppDepConfig {
private void disablePythonAndOpenCV(String reason) {
List<String> pythonFeatures = getAffectedFeatures("Python");
List<String> openCVFeatures = getAffectedFeatures("OpenCV");
endpointConfiguration.disableGroup("Python");
endpointConfiguration.disableGroup("OpenCV");
endpointConfiguration.disableGroup(
"Python", EndpointConfiguration.DisableReason.DEPENDENCY);
endpointConfiguration.disableGroup(
"OpenCV", EndpointConfiguration.DisableReason.DEPENDENCY);
log.warn(
"Missing dependency: Python (reason: {}) - Disabling Python features: {} and OpenCV"
+ " features: {}",
@@ -106,16 +106,29 @@ public class WebMvcConfig implements WebMvcConfigurer {
.allowCredentials(true)
.maxAge(3600);
} else if (hasConfiguredOrigins) {
// Use user-configured origins
// Use user-configured origins + always include Tauri origins for desktop app support
logger.info(
"Configuring CORS with allowed origins: {}",
applicationProperties.getSystem().getCorsAllowedOrigins());
String[] allowedOrigins =
applicationProperties
.getSystem()
.getCorsAllowedOrigins()
.toArray(new String[0]);
// Combine user-configured origins with Tauri origins
java.util.List<String> allOrigins =
new java.util.ArrayList<>(
applicationProperties.getSystem().getCorsAllowedOrigins());
// Always include Tauri origins for desktop app compatibility
// Tauri v1 uses tauri://localhost, v2 uses http(s)://tauri.localhost
if (!allOrigins.contains("tauri://localhost")) {
allOrigins.add("tauri://localhost");
}
if (!allOrigins.contains("http://tauri.localhost")) {
allOrigins.add("http://tauri.localhost");
}
if (!allOrigins.contains("https://tauri.localhost")) {
allOrigins.add("https://tauri.localhost");
}
String[] allowedOrigins = allOrigins.toArray(new String[0]);
registry.addMapping("/**")
.allowedOriginPatterns(allowedOrigins)
@@ -18,26 +18,22 @@ import org.apache.pdfbox.rendering.PDFRenderer;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.api.general.CropPdfForm;
import stirling.software.common.annotations.AutoJobPostMapping;
import stirling.software.common.annotations.api.GeneralApi;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.ProcessExecutor;
import stirling.software.common.util.WebResponseUtils;
@RestController
@RequestMapping("/api/v1/general")
@Tag(name = "General", description = "General APIs")
@GeneralApi
@RequiredArgsConstructor
@Slf4j
public class CropController {
@@ -122,7 +118,7 @@ public class CropController {
return r >= threshold && g >= threshold && b >= threshold;
}
@PostMapping(value = "/crop", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@AutoJobPostMapping(value = "/crop", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@Operation(
summary = "Crops a PDF document",
description =
@@ -19,7 +19,6 @@ import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@@ -27,36 +26,38 @@ import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.api.EditTableOfContentsRequest;
import stirling.software.common.annotations.AutoJobPostMapping;
import stirling.software.common.annotations.api.GeneralApi;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.WebResponseUtils;
@RestController
@RequestMapping("/api/v1/general")
@GeneralApi
@Slf4j
@Tag(name = "General", description = "General APIs")
@RequiredArgsConstructor
public class EditTableOfContentsController {
private final CustomPDFDocumentFactory pdfDocumentFactory;
private final ObjectMapper objectMapper;
@PostMapping(value = "/extract-bookmarks", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@AutoJobPostMapping(
value = "/extract-bookmarks",
consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@Operation(
summary = "Extract PDF Bookmarks",
description = "Extracts bookmarks/table of contents from a PDF document as JSON.")
@ResponseBody
public List<Map<String, Object>> extractBookmarks(@RequestParam("file") MultipartFile file)
public ResponseEntity<List<Map<String, Object>>> extractBookmarks(@RequestParam("file") MultipartFile file)
throws Exception {
try (PDDocument document = pdfDocumentFactory.load(file)) {
PDDocumentOutline outline = document.getDocumentCatalog().getDocumentOutline();
if (outline == null) {
log.info("No outline/bookmarks found in PDF");
return new ArrayList<>();
return ResponseEntity.ok(new ArrayList<>());
}
return extractBookmarkItems(document, outline);
List<Map<String, Object>> bookmarks = extractBookmarkItems(document, outline);
return ResponseEntity.ok(bookmarks);
}
}
@@ -142,7 +143,9 @@ public class EditTableOfContentsController {
return bookmark;
}
@PostMapping(value = "/edit-table-of-contents", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@AutoJobPostMapping(
value = "/edit-table-of-contents",
consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@Operation(
summary = "Edit Table of Contents",
description = "Add or edit bookmarks/table of contents in a PDF document.")
@@ -14,34 +14,32 @@ import org.apache.pdfbox.util.Matrix;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.api.general.MergeMultiplePagesRequest;
import stirling.software.common.annotations.AutoJobPostMapping;
import stirling.software.common.annotations.api.GeneralApi;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.GeneralFormCopyUtils;
import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.WebResponseUtils;
@RestController
@RequestMapping("/api/v1/general")
@Tag(name = "General", description = "General APIs")
@GeneralApi
@RequiredArgsConstructor
@Slf4j
public class MultiPageLayoutController {
private final CustomPDFDocumentFactory pdfDocumentFactory;
@PostMapping(value = "/multi-page-layout", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@AutoJobPostMapping(
value = "/multi-page-layout",
consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@Operation(
summary = "Merge multiple pages of a PDF document into a single page",
description =
@@ -15,26 +15,22 @@ import org.apache.pdfbox.util.Matrix;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.api.general.ScalePagesRequest;
import stirling.software.common.annotations.AutoJobPostMapping;
import stirling.software.common.annotations.api.GeneralApi;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.WebResponseUtils;
@RestController
@RequestMapping("/api/v1/general")
@Tag(name = "General", description = "General APIs")
@GeneralApi
@Slf4j
@RequiredArgsConstructor
public class ScalePagesController {
@@ -116,7 +112,7 @@ public class ScalePagesController {
return sizeMap;
}
@PostMapping(value = "/scale-pages", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@AutoJobPostMapping(value = "/scale-pages", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@Operation(
summary = "Change the size of a PDF page/document",
description =
@@ -1,14 +1,11 @@
package stirling.software.SPDF.controller.api;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import io.swagger.v3.oas.annotations.Hidden;
@@ -52,385 +49,4 @@ public class SettingsController {
public ResponseEntity<Map<String, Boolean>> getDisabledEndpoints() {
return ResponseEntity.ok(endpointConfiguration.getEndpointStatuses());
}
// ========== GENERAL SETTINGS ==========
@GetMapping("/admin/settings/general")
@Hidden
public ResponseEntity<Map<String, Object>> getGeneralSettings() {
Map<String, Object> settings = new HashMap<>();
settings.put("ui", applicationProperties.getUi());
settings.put(
"system",
Map.of(
"defaultLocale", applicationProperties.getSystem().getDefaultLocale(),
"showUpdate", applicationProperties.getSystem().isShowUpdate(),
"showUpdateOnlyAdmin",
applicationProperties.getSystem().isShowUpdateOnlyAdmin(),
"customHTMLFiles", applicationProperties.getSystem().isCustomHTMLFiles(),
"fileUploadLimit", applicationProperties.getSystem().getFileUploadLimit()));
return ResponseEntity.ok(settings);
}
@PostMapping("/admin/settings/general")
@Hidden
public ResponseEntity<String> updateGeneralSettings(@RequestBody Map<String, Object> settings)
throws IOException {
// Update UI settings
if (settings.containsKey("ui")) {
Map<String, String> ui = (Map<String, String>) settings.get("ui");
if (ui.containsKey("appNameNavbar")) {
GeneralUtils.saveKeyToSettings("ui.appNameNavbar", ui.get("appNameNavbar"));
applicationProperties.getUi().setAppNameNavbar(ui.get("appNameNavbar"));
}
}
// Update System settings
if (settings.containsKey("system")) {
Map<String, Object> system = (Map<String, Object>) settings.get("system");
if (system.containsKey("defaultLocale")) {
GeneralUtils.saveKeyToSettings("system.defaultLocale", system.get("defaultLocale"));
applicationProperties
.getSystem()
.setDefaultLocale((String) system.get("defaultLocale"));
}
if (system.containsKey("showUpdate")) {
GeneralUtils.saveKeyToSettings("system.showUpdate", system.get("showUpdate"));
applicationProperties.getSystem().setShowUpdate((Boolean) system.get("showUpdate"));
}
if (system.containsKey("showUpdateOnlyAdmin")) {
GeneralUtils.saveKeyToSettings(
"system.showUpdateOnlyAdmin", system.get("showUpdateOnlyAdmin"));
applicationProperties
.getSystem()
.setShowUpdateOnlyAdmin((Boolean) system.get("showUpdateOnlyAdmin"));
}
if (system.containsKey("fileUploadLimit")) {
GeneralUtils.saveKeyToSettings(
"system.fileUploadLimit", system.get("fileUploadLimit"));
applicationProperties
.getSystem()
.setFileUploadLimit((String) system.get("fileUploadLimit"));
}
}
return ResponseEntity.ok(
"General settings updated. Restart required for changes to take effect.");
}
// ========== SECURITY SETTINGS ==========
@GetMapping("/admin/settings/security")
@Hidden
public ResponseEntity<Map<String, Object>> getSecuritySettings() {
Map<String, Object> settings = new HashMap<>();
ApplicationProperties.Security security = applicationProperties.getSecurity();
settings.put("enableLogin", security.isEnableLogin());
settings.put("loginMethod", security.getLoginMethod());
settings.put("loginAttemptCount", security.getLoginAttemptCount());
settings.put("loginResetTimeMinutes", security.getLoginResetTimeMinutes());
settings.put(
"initialLogin",
Map.of(
"username",
security.getInitialLogin().getUsername() != null
? security.getInitialLogin().getUsername()
: ""));
// JWT settings
ApplicationProperties.Security.Jwt jwt = security.getJwt();
settings.put(
"jwt",
Map.of(
"enableKeystore", jwt.isEnableKeystore(),
"enableKeyRotation", jwt.isEnableKeyRotation(),
"enableKeyCleanup", jwt.isEnableKeyCleanup(),
"keyRetentionDays", jwt.getKeyRetentionDays()));
return ResponseEntity.ok(settings);
}
@PostMapping("/admin/settings/security")
@Hidden
public ResponseEntity<String> updateSecuritySettings(@RequestBody Map<String, Object> settings)
throws IOException {
if (settings.containsKey("enableLogin")) {
GeneralUtils.saveKeyToSettings("security.enableLogin", settings.get("enableLogin"));
applicationProperties
.getSecurity()
.setEnableLogin((Boolean) settings.get("enableLogin"));
}
if (settings.containsKey("loginMethod")) {
GeneralUtils.saveKeyToSettings("security.loginMethod", settings.get("loginMethod"));
applicationProperties
.getSecurity()
.setLoginMethod((String) settings.get("loginMethod"));
}
if (settings.containsKey("loginAttemptCount")) {
GeneralUtils.saveKeyToSettings(
"security.loginAttemptCount", settings.get("loginAttemptCount"));
applicationProperties
.getSecurity()
.setLoginAttemptCount((Integer) settings.get("loginAttemptCount"));
}
if (settings.containsKey("loginResetTimeMinutes")) {
GeneralUtils.saveKeyToSettings(
"security.loginResetTimeMinutes", settings.get("loginResetTimeMinutes"));
applicationProperties
.getSecurity()
.setLoginResetTimeMinutes(
((Number) settings.get("loginResetTimeMinutes")).longValue());
}
// JWT settings
if (settings.containsKey("jwt")) {
Map<String, Object> jwt = (Map<String, Object>) settings.get("jwt");
if (jwt.containsKey("keyRetentionDays")) {
GeneralUtils.saveKeyToSettings(
"security.jwt.keyRetentionDays", jwt.get("keyRetentionDays"));
applicationProperties
.getSecurity()
.getJwt()
.setKeyRetentionDays((Integer) jwt.get("keyRetentionDays"));
}
}
return ResponseEntity.ok(
"Security settings updated. Restart required for changes to take effect.");
}
// ========== CONNECTIONS SETTINGS (OAuth/SAML) ==========
@GetMapping("/admin/settings/connections")
@Hidden
public ResponseEntity<Map<String, Object>> getConnectionsSettings() {
Map<String, Object> settings = new HashMap<>();
ApplicationProperties.Security security = applicationProperties.getSecurity();
// OAuth2 settings
ApplicationProperties.Security.OAUTH2 oauth2 = security.getOauth2();
settings.put(
"oauth2",
Map.of(
"enabled", oauth2.getEnabled(),
"issuer", oauth2.getIssuer() != null ? oauth2.getIssuer() : "",
"clientId", oauth2.getClientId() != null ? oauth2.getClientId() : "",
"provider", oauth2.getProvider() != null ? oauth2.getProvider() : "",
"autoCreateUser", oauth2.getAutoCreateUser(),
"blockRegistration", oauth2.getBlockRegistration(),
"useAsUsername",
oauth2.getUseAsUsername() != null
? oauth2.getUseAsUsername()
: ""));
// SAML2 settings
ApplicationProperties.Security.SAML2 saml2 = security.getSaml2();
settings.put(
"saml2",
Map.of(
"enabled", saml2.getEnabled(),
"provider", saml2.getProvider() != null ? saml2.getProvider() : "",
"autoCreateUser", saml2.getAutoCreateUser(),
"blockRegistration", saml2.getBlockRegistration(),
"registrationId", saml2.getRegistrationId()));
return ResponseEntity.ok(settings);
}
@PostMapping("/admin/settings/connections")
@Hidden
public ResponseEntity<String> updateConnectionsSettings(
@RequestBody Map<String, Object> settings) throws IOException {
// OAuth2 settings
if (settings.containsKey("oauth2")) {
Map<String, Object> oauth2 = (Map<String, Object>) settings.get("oauth2");
if (oauth2.containsKey("enabled")) {
GeneralUtils.saveKeyToSettings("security.oauth2.enabled", oauth2.get("enabled"));
applicationProperties
.getSecurity()
.getOauth2()
.setEnabled((Boolean) oauth2.get("enabled"));
}
if (oauth2.containsKey("issuer")) {
GeneralUtils.saveKeyToSettings("security.oauth2.issuer", oauth2.get("issuer"));
applicationProperties
.getSecurity()
.getOauth2()
.setIssuer((String) oauth2.get("issuer"));
}
if (oauth2.containsKey("clientId")) {
GeneralUtils.saveKeyToSettings("security.oauth2.clientId", oauth2.get("clientId"));
applicationProperties
.getSecurity()
.getOauth2()
.setClientId((String) oauth2.get("clientId"));
}
if (oauth2.containsKey("clientSecret")) {
GeneralUtils.saveKeyToSettings(
"security.oauth2.clientSecret", oauth2.get("clientSecret"));
applicationProperties
.getSecurity()
.getOauth2()
.setClientSecret((String) oauth2.get("clientSecret"));
}
if (oauth2.containsKey("provider")) {
GeneralUtils.saveKeyToSettings("security.oauth2.provider", oauth2.get("provider"));
applicationProperties
.getSecurity()
.getOauth2()
.setProvider((String) oauth2.get("provider"));
}
if (oauth2.containsKey("autoCreateUser")) {
GeneralUtils.saveKeyToSettings(
"security.oauth2.autoCreateUser", oauth2.get("autoCreateUser"));
applicationProperties
.getSecurity()
.getOauth2()
.setAutoCreateUser((Boolean) oauth2.get("autoCreateUser"));
}
if (oauth2.containsKey("blockRegistration")) {
GeneralUtils.saveKeyToSettings(
"security.oauth2.blockRegistration", oauth2.get("blockRegistration"));
applicationProperties
.getSecurity()
.getOauth2()
.setBlockRegistration((Boolean) oauth2.get("blockRegistration"));
}
if (oauth2.containsKey("useAsUsername")) {
GeneralUtils.saveKeyToSettings(
"security.oauth2.useAsUsername", oauth2.get("useAsUsername"));
applicationProperties
.getSecurity()
.getOauth2()
.setUseAsUsername((String) oauth2.get("useAsUsername"));
}
}
// SAML2 settings
if (settings.containsKey("saml2")) {
Map<String, Object> saml2 = (Map<String, Object>) settings.get("saml2");
if (saml2.containsKey("enabled")) {
GeneralUtils.saveKeyToSettings("security.saml2.enabled", saml2.get("enabled"));
applicationProperties
.getSecurity()
.getSaml2()
.setEnabled((Boolean) saml2.get("enabled"));
}
if (saml2.containsKey("provider")) {
GeneralUtils.saveKeyToSettings("security.saml2.provider", saml2.get("provider"));
applicationProperties
.getSecurity()
.getSaml2()
.setProvider((String) saml2.get("provider"));
}
if (saml2.containsKey("autoCreateUser")) {
GeneralUtils.saveKeyToSettings(
"security.saml2.autoCreateUser", saml2.get("autoCreateUser"));
applicationProperties
.getSecurity()
.getSaml2()
.setAutoCreateUser((Boolean) saml2.get("autoCreateUser"));
}
if (saml2.containsKey("blockRegistration")) {
GeneralUtils.saveKeyToSettings(
"security.saml2.blockRegistration", saml2.get("blockRegistration"));
applicationProperties
.getSecurity()
.getSaml2()
.setBlockRegistration((Boolean) saml2.get("blockRegistration"));
}
}
return ResponseEntity.ok(
"Connection settings updated. Restart required for changes to take effect.");
}
// ========== PRIVACY SETTINGS ==========
@GetMapping("/admin/settings/privacy")
@Hidden
public ResponseEntity<Map<String, Object>> getPrivacySettings() {
Map<String, Object> settings = new HashMap<>();
settings.put("enableAnalytics", applicationProperties.getSystem().getEnableAnalytics());
settings.put("googleVisibility", applicationProperties.getSystem().isGooglevisibility());
settings.put("metricsEnabled", applicationProperties.getMetrics().isEnabled());
return ResponseEntity.ok(settings);
}
@PostMapping("/admin/settings/privacy")
@Hidden
public ResponseEntity<String> updatePrivacySettings(@RequestBody Map<String, Object> settings)
throws IOException {
if (settings.containsKey("enableAnalytics")) {
GeneralUtils.saveKeyToSettings(
"system.enableAnalytics", settings.get("enableAnalytics"));
applicationProperties
.getSystem()
.setEnableAnalytics((Boolean) settings.get("enableAnalytics"));
}
if (settings.containsKey("googleVisibility")) {
GeneralUtils.saveKeyToSettings(
"system.googlevisibility", settings.get("googleVisibility"));
applicationProperties
.getSystem()
.setGooglevisibility((Boolean) settings.get("googleVisibility"));
}
if (settings.containsKey("metricsEnabled")) {
GeneralUtils.saveKeyToSettings("metrics.enabled", settings.get("metricsEnabled"));
applicationProperties.getMetrics().setEnabled((Boolean) settings.get("metricsEnabled"));
}
return ResponseEntity.ok(
"Privacy settings updated. Restart required for changes to take effect.");
}
// ========== ADVANCED SETTINGS ==========
@GetMapping("/admin/settings/advanced")
@Hidden
public ResponseEntity<Map<String, Object>> getAdvancedSettings() {
Map<String, Object> settings = new HashMap<>();
settings.put("endpoints", applicationProperties.getEndpoints());
settings.put(
"enableAlphaFunctionality",
applicationProperties.getSystem().isEnableAlphaFunctionality());
settings.put("maxDPI", applicationProperties.getSystem().getMaxDPI());
settings.put("enableUrlToPDF", applicationProperties.getSystem().isEnableUrlToPDF());
settings.put("customPaths", applicationProperties.getSystem().getCustomPaths());
settings.put(
"tempFileManagement", applicationProperties.getSystem().getTempFileManagement());
return ResponseEntity.ok(settings);
}
@PostMapping("/admin/settings/advanced")
@Hidden
public ResponseEntity<String> updateAdvancedSettings(@RequestBody Map<String, Object> settings)
throws IOException {
if (settings.containsKey("enableAlphaFunctionality")) {
GeneralUtils.saveKeyToSettings(
"system.enableAlphaFunctionality", settings.get("enableAlphaFunctionality"));
applicationProperties
.getSystem()
.setEnableAlphaFunctionality(
(Boolean) settings.get("enableAlphaFunctionality"));
}
if (settings.containsKey("maxDPI")) {
GeneralUtils.saveKeyToSettings("system.maxDPI", settings.get("maxDPI"));
applicationProperties.getSystem().setMaxDPI((Integer) settings.get("maxDPI"));
}
if (settings.containsKey("enableUrlToPDF")) {
GeneralUtils.saveKeyToSettings("system.enableUrlToPDF", settings.get("enableUrlToPDF"));
applicationProperties
.getSystem()
.setEnableUrlToPDF((Boolean) settings.get("enableUrlToPDF"));
}
return ResponseEntity.ok(
"Advanced settings updated. Restart required for changes to take effect.");
}
}
@@ -15,20 +15,18 @@ import org.apache.pdfbox.pdmodel.PDDocument;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import io.github.pixee.security.Filenames;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.EndpointConfiguration;
import stirling.software.SPDF.model.api.converters.ConvertEbookToPdfRequest;
import stirling.software.common.annotations.AutoJobPostMapping;
import stirling.software.common.annotations.api.ConvertApi;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.ProcessExecutor;
@@ -36,9 +34,7 @@ import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils;
@RestController
@RequestMapping("/api/v1/convert")
@Tag(name = "Convert", description = "Convert APIs")
@ConvertApi
@RequiredArgsConstructor
@Slf4j
public class ConvertEbookToPDFController {
@@ -58,7 +54,7 @@ public class ConvertEbookToPDFController {
return endpointConfiguration.isGroupEnabled("Ghostscript");
}
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/ebook/pdf")
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/ebook/pdf")
@Operation(
summary = "Convert an eBook file to PDF",
description =
@@ -97,8 +97,6 @@ public class ConvertOfficeController {
// Unoconvert: schreibe direkt in outputPath innerhalb des workDir
List<String> command = new ArrayList<>();
command.add(runtimePathConfig.getUnoConvertPath());
command.add("--port");
command.add("2003");
command.add("--convert-to");
command.add("pdf");
command.add(inputPath.toString());
@@ -12,14 +12,10 @@ import org.apache.commons.io.FilenameUtils;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import io.github.pixee.security.Filenames;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -28,15 +24,15 @@ import stirling.software.SPDF.config.EndpointConfiguration;
import stirling.software.SPDF.model.api.converters.ConvertPdfToEpubRequest;
import stirling.software.SPDF.model.api.converters.ConvertPdfToEpubRequest.OutputFormat;
import stirling.software.SPDF.model.api.converters.ConvertPdfToEpubRequest.TargetDevice;
import stirling.software.common.annotations.AutoJobPostMapping;
import stirling.software.common.annotations.api.ConvertApi;
import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.ProcessExecutor;
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils;
@RestController
@RequestMapping("/api/v1/convert")
@Tag(name = "Convert", description = "Convert APIs")
@ConvertApi
@RequiredArgsConstructor
@Slf4j
public class ConvertPDFToEpubController {
@@ -77,7 +73,7 @@ public class ConvertPDFToEpubController {
return command;
}
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/epub")
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/epub")
@Operation(
summary = "Convert PDF to EPUB/AZW3",
description =
@@ -3,31 +3,27 @@ package stirling.software.SPDF.controller.api.converters;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import stirling.software.common.annotations.AutoJobPostMapping;
import stirling.software.common.annotations.api.ConvertApi;
import stirling.software.common.configuration.RuntimePathConfig;
import stirling.software.common.model.api.PDFFile;
import stirling.software.common.util.PDFToFile;
import stirling.software.common.util.TempFileManager;
@RestController
@Tag(name = "Convert", description = "Convert APIs")
@RequestMapping("/api/v1/convert")
@ConvertApi
@RequiredArgsConstructor
public class ConvertPDFToHtml {
private final TempFileManager tempFileManager;
private final RuntimePathConfig runtimePathConfig;
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/html")
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/html")
@Operation(
summary = "Convert PDF to HTML",
description =
@@ -7,19 +7,17 @@ import org.apache.pdfbox.text.PDFTextStripper;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import stirling.software.SPDF.model.api.converters.PdfToPresentationRequest;
import stirling.software.SPDF.model.api.converters.PdfToTextOrRTFRequest;
import stirling.software.SPDF.model.api.converters.PdfToWordRequest;
import stirling.software.common.annotations.AutoJobPostMapping;
import stirling.software.common.annotations.api.ConvertApi;
import stirling.software.common.configuration.RuntimePathConfig;
import stirling.software.common.model.api.PDFFile;
import stirling.software.common.service.CustomPDFDocumentFactory;
@@ -28,9 +26,7 @@ import stirling.software.common.util.PDFToFile;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils;
@RestController
@RequestMapping("/api/v1/convert")
@Tag(name = "Convert", description = "Convert APIs")
@ConvertApi
@RequiredArgsConstructor
public class ConvertPDFToOffice {
@@ -38,7 +34,7 @@ public class ConvertPDFToOffice {
private final TempFileManager tempFileManager;
private final RuntimePathConfig runtimePathConfig;
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/presentation")
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/presentation")
@Operation(
summary = "Convert PDF to Presentation format",
description =
@@ -53,7 +49,7 @@ public class ConvertPDFToOffice {
return pdfToFile.processPdfToOfficeFormat(inputFile, outputFormat, "impress_pdf_import");
}
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/text")
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/text")
@Operation(
summary = "Convert PDF to Text or RTF format",
description =
@@ -79,7 +75,7 @@ public class ConvertPDFToOffice {
}
}
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/word")
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/word")
@Operation(
summary = "Convert PDF to Word document",
description =
@@ -93,7 +89,7 @@ public class ConvertPDFToOffice {
return pdfToFile.processPdfToOfficeFormat(inputFile, outputFormat, "writer_pdf_import");
}
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/xml")
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/xml")
@Operation(
summary = "Convert PDF to XML",
description =
@@ -74,30 +74,26 @@ import org.apache.xmpbox.xml.XmpSerializer;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import io.github.pixee.security.Filenames;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.api.converters.PdfToPdfARequest;
import stirling.software.common.annotations.AutoJobPostMapping;
import stirling.software.common.annotations.api.ConvertApi;
import stirling.software.common.configuration.RuntimePathConfig;
import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.ProcessExecutor;
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
import stirling.software.common.util.WebResponseUtils;
@RestController
@RequestMapping("/api/v1/convert")
@ConvertApi
@Slf4j
@Tag(name = "Convert", description = "Convert APIs")
@RequiredArgsConstructor
public class ConvertPDFToPDFA {
@@ -483,15 +479,22 @@ public class ConvertPDFToPDFA {
command.add("-dCompatibilityLevel=" + profile.getCompatibilityLevel());
command.add("-sDEVICE=pdfwrite");
command.add("-sColorConversionStrategy=RGB");
command.add("-dProcessColorModel=/DeviceRGB");
command.add("-sOutputICCProfile=" + colorProfiles.rgb().toAbsolutePath());
command.add("-sDefaultRGBProfile=" + colorProfiles.rgb().toAbsolutePath());
command.add("-sDefaultGrayProfile=" + colorProfiles.gray().toAbsolutePath());
command.add("-dEmbedAllFonts=true");
command.add("-dSubsetFonts=false"); // Embed complete fonts to avoid incomplete glyphs
command.add("-dSubsetFonts=true");
command.add("-dCompressFonts=true");
command.add("-dNOSUBSTFONTS=false"); // Allow font substitution for problematic fonts
command.add("-dPDFSETTINGS=/prepress");
// Explicitly tune downsampling/compression for high-quality print
command.add("-dColorImageDownsampleType=/Bicubic");
command.add("-dColorImageResolution=300");
command.add("-dGrayImageDownsampleType=/Bicubic");
command.add("-dGrayImageResolution=300");
command.add("-dMonoImageDownsampleType=/Bicubic");
command.add("-dMonoImageResolution=1200");
command.add("-dNOPAUSE");
command.add("-dBATCH");
command.add("-dNOOUTERSAVE");
@@ -562,7 +565,7 @@ public class ConvertPDFToPDFA {
}
}
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/pdfa")
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/pdfa")
@Operation(
summary = "Convert a PDF to a PDF/A or PDF/X",
description =
@@ -2449,9 +2452,7 @@ public class ConvertPDFToPDFA {
@Getter
private enum PdfXProfile {
PDF_X_1("PDF/X-1", "_PDFX-1.pdf", "1.3", "2001", "pdfx-1", "pdfx"),
PDF_X_3("PDF/X-3", "_PDFX-3.pdf", "1.3", "2003", "pdfx-3"),
PDF_X_4("PDF/X-4", "_PDFX-4.pdf", "1.4", "2008", "pdfx-4");
PDF_X("PDF/X", "_PDFX.pdf", "1.6", "2008", "pdfx");
private final String displayName;
private final String suffix;
@@ -2477,7 +2478,7 @@ public class ConvertPDFToPDFA {
static PdfXProfile fromRequest(String requestToken) {
if (requestToken == null) {
return PDF_X_4;
return PDF_X;
}
String normalized = requestToken.trim().toLowerCase(Locale.ROOT);
Optional<PdfXProfile> match =
@@ -2485,7 +2486,7 @@ public class ConvertPDFToPDFA {
.filter(profile -> profile.requestTokens.contains(normalized))
.findFirst();
return match.orElse(PDF_X_4);
return match.orElse(PDF_X);
}
String outputSuffix() {
@@ -9,7 +9,6 @@ import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
@@ -89,7 +88,7 @@ public class ConvertPdfJsonController {
return WebResponseUtils.bytesToWebResponse(pdfBytes, docName);
}
@PostMapping(consumes = "multipart/form-data", value = "/pdf/text-editor/metadata")
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/pdf/text-editor/metadata")
@Operation(
summary = "Extract PDF metadata for text editor lazy loading",
description =
@@ -127,7 +126,7 @@ public class ConvertPdfJsonController {
.body(jsonBytes);
}
@PostMapping(
@AutoJobPostMapping(
value = "/pdf/text-editor/partial/{jobId}",
consumes = MediaType.APPLICATION_JSON_VALUE)
@StandardPdfResponse
@@ -180,7 +179,7 @@ public class ConvertPdfJsonController {
return WebResponseUtils.bytesToWebResponse(jsonBytes, docName, MediaType.APPLICATION_JSON);
}
@PostMapping(value = "/pdf/text-editor/clear-cache/{jobId}")
@AutoJobPostMapping(value = "/pdf/text-editor/clear-cache/{jobId}")
@Operation(
summary = "Clear cached PDF document for text editor",
description =
@@ -27,18 +27,16 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import io.github.pixee.security.Filenames;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import stirling.software.SPDF.model.api.converters.PdfToVideoRequest;
import stirling.software.common.annotations.AutoJobPostMapping;
import stirling.software.common.annotations.api.ConvertApi;
import stirling.software.common.model.ApplicationProperties;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.ApplicationContextProvider;
@@ -51,9 +49,7 @@ import stirling.software.common.util.TempFile;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils;
@RestController
@RequestMapping("/api/v1/convert")
@Tag(name = "Convert", description = "Convert APIs")
@ConvertApi
@RequiredArgsConstructor
public class ConvertPdfToVideoController {
@@ -68,7 +64,7 @@ public class ConvertPdfToVideoController {
private final CustomPDFDocumentFactory pdfDocumentFactory;
private final TempFileManager tempFileManager;
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/video")
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/video")
@Operation(
summary = "Convert PDF to Video Slideshow",
description =
@@ -0,0 +1,244 @@
package stirling.software.SPDF.controller.api.converters;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.multipart.MultipartFile;
import io.github.pixee.security.Filenames;
import io.swagger.v3.oas.annotations.Operation;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.swagger.MultiFileResponse;
import stirling.software.SPDF.model.api.converters.SvgToPdfRequest;
import stirling.software.SPDF.utils.SvgToPdf;
import stirling.software.common.annotations.AutoJobPostMapping;
import stirling.software.common.annotations.api.ConvertApi;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.SvgSanitizer;
import stirling.software.common.util.TempFile;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils;
@ConvertApi
@Slf4j
@RequiredArgsConstructor
public class ConvertSvgToPDF {
private final CustomPDFDocumentFactory pdfDocumentFactory;
private final SvgSanitizer svgSanitizer;
private final TempFileManager tempFileManager;
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/svg/pdf")
@MultiFileResponse
@Operation(
summary = "Convert SVG to PDF",
description =
"This endpoint converts one or more SVG (Scalable Vector Graphics) files to PDF format. "
+ "Each SVG is converted to a separate PDF file. "
+ "The conversion preserves vector graphics for crisp output at any resolution - no rasterization occurs. "
+ "SVG dimensions (width/height) determine the PDF page size; defaults to A4 if not specified. "
+ "SVG content is sanitized to prevent XSS attacks. "
+ "Input: SVG file(s), Output: PDF file(s) or ZIP. Type: MIMO")
public ResponseEntity<byte[]> convertSvgToPdf(@ModelAttribute SvgToPdfRequest request) {
MultipartFile[] inputFiles = request.getFileInput();
boolean combineIntoSinglePdf = Boolean.TRUE.equals(request.getCombineIntoSinglePdf());
// Validate input
if (inputFiles == null || inputFiles.length == 0) {
log.error("No files provided for SVG to PDF conversion.");
return ResponseEntity.badRequest()
.body("No files provided".getBytes(StandardCharsets.UTF_8));
}
try {
List<byte[]> sanitizedSvgs = new ArrayList<>();
List<String> filenames = new ArrayList<>();
for (MultipartFile inputFile : inputFiles) {
if (inputFile == null || inputFile.isEmpty()) {
log.warn("Skipping empty file in batch conversion");
continue;
}
String originalFilename = inputFile.getOriginalFilename();
if (originalFilename == null || originalFilename.trim().isEmpty()) {
log.warn("Skipping file with null or empty filename");
continue;
}
String lowerFilename = originalFilename.toLowerCase(Locale.ROOT);
if (!lowerFilename.endsWith(".svg")) {
log.warn("Skipping non-SVG file: {}", originalFilename);
continue;
}
try {
byte[] fileBytes = inputFile.getBytes();
byte[] sanitizedBytes = svgSanitizer.sanitize(fileBytes);
sanitizedSvgs.add(sanitizedBytes);
filenames.add(Filenames.toSimpleFileName(originalFilename));
} catch (IOException e) {
log.error(
"SVG sanitization/reading failed for {}: {}",
originalFilename,
e.getMessage());
}
}
if (sanitizedSvgs.isEmpty()) {
log.error("No valid SVG files were found");
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body("No valid SVG files were found".getBytes(StandardCharsets.UTF_8));
}
if (combineIntoSinglePdf) {
return handleCombinedConversion(sanitizedSvgs, filenames);
} else {
return handleSeparateConversion(sanitizedSvgs, filenames);
}
} catch (Exception e) {
log.error("Unexpected error during SVG to PDF conversion", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(
"An unexpected error occurred during conversion"
.getBytes(StandardCharsets.UTF_8));
}
}
private ResponseEntity<byte[]> handleCombinedConversion(
List<byte[]> sanitizedSvgs, List<String> filenames) {
try {
log.info("Combining {} SVG files into single PDF", sanitizedSvgs.size());
byte[] pdfBytes = SvgToPdf.combineIntoPdf(sanitizedSvgs);
if (pdfBytes == null || pdfBytes.length == 0) {
log.error("PDF conversion failed - empty output");
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(
"PDF conversion failed - empty output"
.getBytes(StandardCharsets.UTF_8));
}
pdfBytes = pdfDocumentFactory.createNewBytesBasedOnOldDocument(pdfBytes);
String outputFilename =
filenames.isEmpty()
? "combined_svgs.pdf"
: GeneralUtils.generateFilename(filenames.get(0), "_combined.pdf");
log.info("Successfully combined {} SVGs into single PDF", sanitizedSvgs.size());
return WebResponseUtils.bytesToWebResponse(
pdfBytes, outputFilename, MediaType.APPLICATION_PDF);
} catch (IOException e) {
log.error("Error combining SVGs into PDF", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(
("Conversion failed: " + e.getMessage())
.getBytes(StandardCharsets.UTF_8));
}
}
private ResponseEntity<byte[]> handleSeparateConversion(
List<byte[]> sanitizedSvgs, List<String> filenames) {
List<ConvertedPdf> convertedPdfs = new ArrayList<>();
for (int i = 0; i < sanitizedSvgs.size(); i++) {
byte[] sanitizedBytes = sanitizedSvgs.get(i);
String baseFilename = filenames.get(i);
try {
byte[] pdfBytes = SvgToPdf.convert(sanitizedBytes);
if (pdfBytes == null || pdfBytes.length == 0) {
log.error("PDF conversion failed - empty output for {}", baseFilename);
continue;
}
pdfBytes = pdfDocumentFactory.createNewBytesBasedOnOldDocument(pdfBytes);
String outputFilename = GeneralUtils.generateFilename(baseFilename, ".pdf");
convertedPdfs.add(new ConvertedPdf(outputFilename, pdfBytes));
log.info("Successfully converted SVG to PDF: {}", baseFilename);
} catch (IOException e) {
log.error("File processing error for SVG to PDF: {}", baseFilename, e);
}
}
if (convertedPdfs.isEmpty()) {
log.error("No files were successfully converted");
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("No files were successfully converted".getBytes(StandardCharsets.UTF_8));
}
try {
if (convertedPdfs.size() == 1) {
ConvertedPdf pdf = convertedPdfs.get(0);
return WebResponseUtils.bytesToWebResponse(
pdf.content, pdf.filename, MediaType.APPLICATION_PDF);
}
String zipFilename =
filenames.isEmpty()
? "converted_svgs.zip"
: GeneralUtils.generateFilename(
filenames.get(0), "_converted_svgs.zip");
byte[] zipBytes = createZipFromPdfs(convertedPdfs);
return WebResponseUtils.bytesToWebResponse(
zipBytes, zipFilename, MediaType.APPLICATION_OCTET_STREAM);
} catch (IOException e) {
log.error("Failed to create response", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("Failed to create response".getBytes(StandardCharsets.UTF_8));
}
}
private byte[] createZipFromPdfs(List<ConvertedPdf> pdfs) throws IOException {
try (TempFile tempZipFile = new TempFile(tempFileManager, ".zip");
ZipOutputStream zipOut =
new ZipOutputStream(Files.newOutputStream(tempZipFile.getPath()))) {
for (ConvertedPdf pdf : pdfs) {
ZipEntry pdfEntry = new ZipEntry(pdf.filename);
zipOut.putNextEntry(pdfEntry);
zipOut.write(pdf.content);
zipOut.closeEntry();
log.debug("Added {} to ZIP", pdf.filename);
}
return Files.readAllBytes(tempZipFile.getPath());
}
}
private static class ConvertedPdf {
final String filename;
final byte[] content;
ConvertedPdf(String filename, byte[] content) {
this.filename = filename;
this.content = content;
}
}
}
@@ -21,19 +21,17 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import org.springframework.web.util.UriComponentsBuilder;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.api.converters.UrlToPdfRequest;
import stirling.software.common.annotations.AutoJobPostMapping;
import stirling.software.common.annotations.api.ConvertApi;
import stirling.software.common.configuration.RuntimePathConfig;
import stirling.software.common.model.ApplicationProperties;
import stirling.software.common.service.CustomPDFDocumentFactory;
@@ -43,10 +41,8 @@ import stirling.software.common.util.ProcessExecutor;
import stirling.software.common.util.RegexPatternUtils;
import stirling.software.common.util.WebResponseUtils;
@RestController
@Tag(name = "Convert", description = "Convert APIs")
@ConvertApi
@Slf4j
@RequestMapping("/api/v1/convert")
@RequiredArgsConstructor
public class ConvertWebsiteToPDF {
@@ -59,7 +55,7 @@ public class ConvertWebsiteToPDF {
private static final Pattern NUMERIC_HTML_ENTITY_PATTERN = Pattern.compile("&#(x?[0-9a-f]+);");
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/url/pdf")
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/url/pdf")
@Operation(
summary = "Convert a URL to a PDF",
description =
@@ -13,12 +13,8 @@ import org.apache.commons.io.FilenameUtils;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
@@ -27,6 +23,8 @@ import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.EndpointConfiguration;
import stirling.software.SPDF.model.api.converters.PdfVectorExportRequest;
import stirling.software.common.annotations.AutoJobPostMapping;
import stirling.software.common.annotations.api.ConvertApi;
import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.ProcessExecutor;
@@ -35,10 +33,8 @@ import stirling.software.common.util.TempFile;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils;
@RestController
@RequestMapping("/api/v1/convert")
@ConvertApi
@Slf4j
@Tag(name = "Convert", description = "Convert APIs")
@RequiredArgsConstructor
public class PdfVectorExportController {
@@ -49,7 +45,7 @@ public class PdfVectorExportController {
private final TempFileManager tempFileManager;
private final EndpointConfiguration endpointConfiguration;
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/vector/pdf")
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/vector/pdf")
@Operation(
summary = "Convert PostScript formats to PDF",
description =
@@ -94,7 +90,7 @@ public class PdfVectorExportController {
}
}
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/vector")
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/vector")
@Operation(
summary = "Convert PDF to vector format",
description =
@@ -8,9 +8,6 @@ import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import io.github.pixee.security.Filenames;
@@ -18,7 +15,6 @@ import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
@@ -28,20 +24,22 @@ import stirling.software.SPDF.model.api.filter.ContainsTextRequest;
import stirling.software.SPDF.model.api.filter.FileSizeRequest;
import stirling.software.SPDF.model.api.filter.PageRotationRequest;
import stirling.software.SPDF.model.api.filter.PageSizeRequest;
import stirling.software.common.annotations.AutoJobPostMapping;
import stirling.software.common.annotations.api.FilterApi;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.PdfUtils;
import stirling.software.common.util.WebResponseUtils;
@RestController
@RequestMapping("/api/v1/filter")
@Tag(name = "Filter", description = "Filter APIs")
@FilterApi
@RequiredArgsConstructor
public class FilterController {
private final CustomPDFDocumentFactory pdfDocumentFactory;
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/filter-contains-text")
@AutoJobPostMapping(
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
value = "/filter-contains-text")
@Operation(
summary = "Checks if a PDF contains set text, returns true if does",
description = "Input:PDF Output:Boolean Type:SISO")
@@ -70,7 +68,9 @@ public class FilterController {
return ResponseEntity.noContent().build();
}
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/filter-contains-image")
@AutoJobPostMapping(
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
value = "/filter-contains-image")
@Operation(
summary = "Checks if a PDF contains an image",
description = "Input:PDF Output:Boolean Type:SISO")
@@ -98,7 +98,9 @@ public class FilterController {
return ResponseEntity.noContent().build();
}
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/filter-page-count")
@AutoJobPostMapping(
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
value = "/filter-page-count")
@Operation(
summary = "Checks if a PDF is greater, less or equal to a setPageCount",
description = "Input:PDF Output:Boolean Type:SISO")
@@ -129,7 +131,7 @@ public class FilterController {
: ResponseEntity.noContent().build();
}
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/filter-page-size")
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/filter-page-size")
@Operation(
summary = "Checks if a PDF is of a certain size",
description = "Input:PDF Output:Boolean Type:SISO")
@@ -166,7 +168,7 @@ public class FilterController {
: ResponseEntity.noContent().build();
}
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/filter-file-size")
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/filter-file-size")
@Operation(
summary = "Checks if a PDF is a set file size",
description = "Input:PDF Output:Boolean Type:SISO")
@@ -194,7 +196,9 @@ public class FilterController {
: ResponseEntity.noContent().build();
}
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/filter-page-rotation")
@AutoJobPostMapping(
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
value = "/filter-page-rotation")
@Operation(
summary = "Checks if a PDF is of a certain rotation",
description = "Input:PDF Output:Boolean Type:SISO")
@@ -36,20 +36,18 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.server.ResponseStatusException;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.*;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.EndpointConfiguration;
import stirling.software.SPDF.model.api.misc.OptimizePdfRequest;
import stirling.software.common.annotations.AutoJobPostMapping;
import stirling.software.common.annotations.api.MiscApi;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.service.LineArtConversionService;
import stirling.software.common.util.ExceptionUtils;
@@ -60,10 +58,8 @@ import stirling.software.common.util.TempFile;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils;
@RestController
@RequestMapping("/api/v1/misc")
@MiscApi
@Slf4j
@Tag(name = "Misc", description = "Miscellaneous APIs")
@RequiredArgsConstructor
public class CompressController {
@@ -922,7 +918,7 @@ public class CompressController {
return Math.min(9, currentLevel + 1);
}
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/compress-pdf")
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/compress-pdf")
@Operation(
summary = "Optimize PDF file",
description =
@@ -112,6 +112,16 @@ public class ConfigController {
"showSettingsWhenNoLogin",
applicationProperties.getSystem().isShowSettingsWhenNoLogin());
// SSO Provider settings
boolean enableOAuth =
applicationProperties.getSecurity().getOauth2() != null
&& applicationProperties.getSecurity().getOauth2().getEnabled();
boolean enableSaml =
applicationProperties.getSecurity().getSaml2() != null
&& applicationProperties.getSecurity().getSaml2().getEnabled();
configData.put("enableOAuth", enableOAuth);
configData.put("enableSaml", enableSaml);
// Mail settings - check both SMTP enabled AND invites enabled
boolean smtpEnabled = applicationProperties.getMail().isEnabled();
boolean invitesEnabled = applicationProperties.getMail().isEnableInvites();
@@ -24,20 +24,18 @@ import org.apache.pdfbox.text.PDFTextStripper;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import io.github.pixee.security.Filenames;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.EndpointConfiguration;
import stirling.software.SPDF.model.api.misc.ProcessPdfWithOcrRequest;
import stirling.software.common.annotations.AutoJobPostMapping;
import stirling.software.common.annotations.api.MiscApi;
import stirling.software.common.configuration.RuntimePathConfig;
import stirling.software.common.model.ApplicationProperties;
import stirling.software.common.service.CustomPDFDocumentFactory;
@@ -50,9 +48,7 @@ import stirling.software.common.util.TempFile;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils;
@RestController
@RequestMapping("/api/v1/misc")
@Tag(name = "Misc", description = "Miscellaneous APIs")
@MiscApi
@Slf4j
@RequiredArgsConstructor
public class OCRController {
@@ -85,7 +81,7 @@ public class OCRController {
.toList();
}
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/ocr-pdf")
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/ocr-pdf")
@Operation(
summary = "Process a PDF file with OCR",
description =
@@ -1,7 +1,12 @@
package stirling.software.SPDF.controller.api.misc;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
@@ -14,11 +19,11 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.api.misc.OverlayImageRequest;
import stirling.software.SPDF.utils.SvgOverlayUtil;
import stirling.software.common.annotations.AutoJobPostMapping;
import stirling.software.common.annotations.api.MiscApi;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.PdfUtils;
import stirling.software.common.util.WebResponseUtils;
@MiscApi
@@ -32,25 +37,63 @@ public class OverlayImageController {
@Operation(
summary = "Overlay image onto a PDF file",
description =
"This endpoint overlays an image onto a PDF file at the specified coordinates."
+ " The image can be overlaid on every page of the PDF if specified. "
+ " Input:PDF/IMAGE Output:PDF Type:SISO")
"This endpoint overlays an image onto a PDF file at the specified coordinates. "
+ "Supports both raster formats (PNG, JPEG, etc.) and vector format (SVG). "
+ "SVG files are rendered as vector graphics for crisp output at any resolution. "
+ "The image can be overlaid on every page of the PDF if specified. "
+ "Input:PDF/IMAGE/SVG Output:PDF Type:SISO")
public ResponseEntity<byte[]> overlayImage(@ModelAttribute OverlayImageRequest request) {
MultipartFile pdfFile = request.getFileInput();
MultipartFile imageFile = request.getImageFile();
float x = request.getX();
float y = request.getY();
boolean everyPage = Boolean.TRUE.equals(request.getEveryPage());
try {
byte[] pdfBytes = pdfFile.getBytes();
byte[] imageBytes = imageFile.getBytes();
byte[] result =
PdfUtils.overlayImage(
pdfDocumentFactory, pdfBytes, imageBytes, x, y, everyPage);
boolean isSvg = SvgOverlayUtil.isSvgImage(imageBytes);
PDDocument document = pdfDocumentFactory.load(pdfBytes);
int pages = document.getNumberOfPages();
for (int i = 0; i < pages; i++) {
PDPage page = document.getPage(i);
if (isSvg) {
SvgOverlayUtil.overlaySvgOnPage(document, page, imageBytes, x, y);
} else {
try (PDPageContentStream contentStream =
new PDPageContentStream(
document,
page,
PDPageContentStream.AppendMode.APPEND,
true,
true)) {
PDImageXObject image =
PDImageXObject.createFromByteArray(document, imageBytes, "");
contentStream.drawImage(image, x, y);
log.info("Image successfully overlaid onto PDF page {}", i);
}
}
if (!everyPage && i == 0) {
break;
}
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
document.save(baos);
document.close();
byte[] result = baos.toByteArray();
log.info("PDF with overlaid image successfully created");
return WebResponseUtils.bytesToWebResponse(
result,
GeneralUtils.generateFilename(pdfFile.getOriginalFilename(), "_overlayed.pdf"));
} catch (IOException e) {
log.error("Failed to add image to PDF", e);
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
@@ -52,9 +52,17 @@ public class PageNumbersController {
int pageNumber = request.getStartingNumber();
String pagesToNumber = request.getPagesToNumber();
String customText = request.getCustomText();
int zeroPad = request.getZeroPad();
float fontSize = request.getFontSize();
String fontType = request.getFontType();
String fontColor = request.getFontColor();
// compute padded number string where requested
String formatN;
if (zeroPad > 0) {
formatN = String.format("%%0%dd", Math.max(0, zeroPad));
} else {
formatN = "%d";
}
Color color = Color.BLACK;
if (fontColor != null && !fontColor.trim().isEmpty()) {
@@ -93,9 +101,10 @@ public class PageNumbersController {
PDPage page = document.getPage(i);
PDRectangle pageSize = page.getMediaBox();
String nFormatted = String.format(formatN, pageNumber);
String text =
customText
.replace("{n}", String.valueOf(pageNumber))
.replace("{n}", nFormatted)
.replace("{total}", String.valueOf(document.getNumberOfPages()))
.replace(
"{filename}",
@@ -7,11 +7,13 @@ import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Locale;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.imageio.ImageIO;
@@ -34,17 +36,15 @@ import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import stirling.software.SPDF.model.api.misc.AddStampRequest;
import stirling.software.common.annotations.AutoJobPostMapping;
import stirling.software.common.annotations.api.MiscApi;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.GeneralUtils;
@@ -53,15 +53,20 @@ import stirling.software.common.util.TempFile;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils;
@RestController
@RequestMapping("/api/v1/misc")
@Tag(name = "Misc", description = "Miscellaneous APIs")
@MiscApi
@RequiredArgsConstructor
public class StampController {
private final CustomPDFDocumentFactory pdfDocumentFactory;
private final TempFileManager tempFileManager;
private static final int MAX_DATE_FORMAT_LENGTH = 50;
private static final Pattern SAFE_DATE_FORMAT_PATTERN =
Pattern.compile("^[yMdHhmsS/\\-:\\s.,'+EGuwWDFzZXa]+$");
private static final Pattern CUSTOM_DATE_PATTERN = Pattern.compile("@date\\{([^}]{1,50})\\}");
// Placeholder for escaped @ symbol (using Unicode private use area)
private static final String ESCAPED_AT_PLACEHOLDER = "\uE000ESCAPED_AT\uE000";
/**
* Initialize data binder for multipart file uploads. This method registers a custom editor for
* MultipartFile to handle file uploads. It sets the MultipartFile to null if the uploaded file
@@ -79,7 +84,7 @@ public class StampController {
});
}
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/add-stamp")
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/add-stamp")
@Operation(
summary = "Add stamp to a PDF file",
description =
@@ -170,7 +175,9 @@ public class StampController {
overrideX,
overrideY,
margin,
customColor);
customColor,
pageIndex,
pdfFileName);
} else if ("image".equalsIgnoreCase(stampType)) {
addImageStamp(
contentStream,
@@ -205,9 +212,11 @@ public class StampController {
float fontSize,
String alphabet,
float overrideX, // X override
float overrideY,
float overrideY, // Y override
float margin,
String colorString) // Y override
String colorString,
int currentPageNumber,
String filename)
throws IOException {
String resourceDir;
PDFont font = new PDType1Font(Standard14Fonts.FontName.HELVETICA);
@@ -235,8 +244,6 @@ public class StampController {
}
}
contentStream.setFont(font, fontSize);
Color redactColor;
try {
if (!colorString.startsWith("#")) {
@@ -244,45 +251,54 @@ public class StampController {
}
redactColor = Color.decode(colorString);
} catch (NumberFormatException e) {
redactColor = Color.LIGHT_GRAY;
}
contentStream.setNonStrokingColor(redactColor);
PDRectangle pageSize = page.getMediaBox();
float x, y;
if (overrideX >= 0 && overrideY >= 0) {
// Use override values if provided
x = overrideX;
y = overrideY;
} else {
x = calculatePositionX(pageSize, position, fontSize, font, fontSize, stampText, margin);
y =
calculatePositionY(
pageSize, position, calculateTextCapHeight(font, fontSize), margin);
}
String currentDate = LocalDate.now().toString();
String currentTime = LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss"));
int pageCount = document.getNumberOfPages();
String processedStampText =
stampText
.replace("@date", currentDate)
.replace("@time", currentTime)
.replace("@page_count", String.valueOf(pageCount));
processStampText(stampText, currentPageNumber, pageCount, filename, document);
// Split the stampText into multiple lines
String[] lines =
RegexPatternUtils.getInstance().getEscapedNewlinePattern().split(stampText);
String normalizedText =
RegexPatternUtils.getInstance()
.getEscapedNewlinePattern()
.matcher(processedStampText)
.replaceAll("\n");
String[] lines = normalizedText.split("\\r?\\n");
PDRectangle pageSize = page.getMediaBox();
// Use fontSize directly (default 40 if not specified)
float effectiveFontSize = fontSize > 0 ? fontSize : 40f;
contentStream.setFont(font, effectiveFontSize);
// Calculate dynamic line height based on font ascent and descent
float ascent = font.getFontDescriptor().getAscent();
float descent = font.getFontDescriptor().getDescent();
float lineHeight = ((ascent - descent) / 1000) * fontSize;
float lineHeight = ((ascent - descent) / 1000) * effectiveFontSize;
float maxLineWidth = 0;
for (String line : lines) {
float lineWidth = calculateTextWidth(line, font, effectiveFontSize);
if (lineWidth > maxLineWidth) {
maxLineWidth = lineWidth;
}
}
float totalTextHeight = lines.length * lineHeight;
float x, y;
if (overrideX >= 0 && overrideY >= 0) {
x = overrideX;
y = overrideY;
} else {
x = calculatePositionX(pageSize, position, maxLineWidth, margin);
y = calculatePositionY(pageSize, position, totalTextHeight, margin);
}
contentStream.beginText();
for (int i = 0; i < lines.length; i++) {
@@ -295,6 +311,140 @@ public class StampController {
contentStream.endText();
}
/**
* Process stamp text by replacing all @commands with their actual values. Supported commands:
*
* <p>Date & Time:
*
* <ul>
* <li>@date - Current date (YYYY-MM-DD)
* <li>@time - Current time (HH:mm:ss)
* <li>@datetime - Current date and time (YYYY-MM-DD HH:mm:ss)
* <li>@date{format} - Custom date/time format (e.g., @date{dd/MM/yyyy})
* <li>@year - Current year (4 digits)
* <li>@month - Current month (01-12)
* <li>@day - Current day of month (01-31)
* </ul>
*
* <p>Page Information:
*
* <ul>
* <li>@page_number or @page - Current page number
* <li>@total_pages or @page_count - Total number of pages
* </ul>
*
* <p>File Information:
*
* <ul>
* <li>@filename - Original filename (without extension)
* <li>@filename_full - Original filename (with extension)
* </ul>
*
* <p>Document Metadata:
*
* <ul>
* <li>@author - Document author (from PDF metadata)
* <li>@title - Document title (from PDF metadata)
* <li>@subject - Document subject (from PDF metadata)
* </ul>
*
* <p>Other:
*
* <ul>
* <li>@uuid - Short unique identifier (8 characters)
* </ul>
*/
private String processStampText(
String stampText,
int currentPageNumber,
int totalPages,
String filename,
PDDocument document) {
if (stampText == null || stampText.isEmpty()) {
return "";
}
// Handle escaped @@ sequences first - replace with placeholder to preserve literal @
String result = stampText.replace("@@", ESCAPED_AT_PLACEHOLDER);
LocalDateTime now = LocalDateTime.now();
String currentDate = now.toLocalDate().toString();
String currentTime = now.toLocalTime().format(DateTimeFormatter.ofPattern("HH:mm:ss"));
String currentDateTime = now.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
String filenameWithoutExt = filename != null ? filename : "";
if (filename != null && filename.contains(".")) {
int lastDot = filename.lastIndexOf('.');
if (lastDot > 0) { // Ensure there's actually a name before the dot
filenameWithoutExt = filename.substring(0, lastDot);
}
}
String author = "";
String title = "";
String subject = "";
if (document != null && document.getDocumentInformation() != null) {
var info = document.getDocumentInformation();
author = info.getAuthor() != null ? info.getAuthor() : "";
title = info.getTitle() != null ? info.getTitle() : "";
subject = info.getSubject() != null ? info.getSubject() : "";
}
String uuid = UUID.randomUUID().toString().substring(0, 8);
// Process @date{format} with custom format first (must be before simple @date)
Matcher matcher = CUSTOM_DATE_PATTERN.matcher(result);
StringBuilder sb = new StringBuilder();
while (matcher.find()) {
String format = matcher.group(1);
String replacement = processCustomDateFormat(format, now);
matcher.appendReplacement(sb, Matcher.quoteReplacement(replacement));
}
matcher.appendTail(sb);
result = sb.toString();
result =
result.replace("@datetime", currentDateTime)
.replace("@date", currentDate)
.replace("@time", currentTime)
.replace("@year", String.valueOf(now.getYear()))
.replace("@month", String.format("%02d", now.getMonthValue()))
.replace("@day", String.format("%02d", now.getDayOfMonth()))
.replace("@page_number", String.valueOf(currentPageNumber))
.replace(
"@page_count", String.valueOf(totalPages)) // Must come before @page
.replace("@total_pages", String.valueOf(totalPages))
.replace(
"@page",
String.valueOf(currentPageNumber)) // Must come after @page_count
.replace("@filename_full", filename != null ? filename : "")
.replace("@filename", filenameWithoutExt)
.replace("@author", author)
.replace("@title", title)
.replace("@subject", subject)
.replace("@uuid", uuid);
result = result.replace(ESCAPED_AT_PLACEHOLDER, "@");
return result;
}
private String processCustomDateFormat(String format, LocalDateTime now) {
if (format == null || format.length() > MAX_DATE_FORMAT_LENGTH) {
return "[invalid format: too long]";
}
if (!SAFE_DATE_FORMAT_PATTERN.matcher(format).matches()) {
return "[invalid format]";
}
try {
return now.format(DateTimeFormatter.ofPattern(format));
} catch (IllegalArgumentException e) {
return "[invalid format: " + format + "]";
}
}
private void addImageStamp(
PDPageContentStream contentStream,
MultipartFile stampImage,
@@ -331,8 +481,8 @@ public class StampController {
x = overrideX;
y = overrideY;
} else {
x = calculatePositionX(pageSize, position, desiredPhysicalWidth, null, 0, null, margin);
y = calculatePositionY(pageSize, position, fontSize, margin);
x = calculatePositionX(pageSize, position, desiredPhysicalWidth, margin);
y = calculatePositionY(pageSize, position, desiredPhysicalHeight, margin);
}
contentStream.saveGraphicsState();
@@ -343,23 +493,14 @@ public class StampController {
}
private float calculatePositionX(
PDRectangle pageSize,
int position,
float contentWidth,
PDFont font,
float fontSize,
String text,
float margin)
throws IOException {
float actualWidth =
(text != null) ? calculateTextWidth(text, font, fontSize) : contentWidth;
PDRectangle pageSize, int position, float contentWidth, float margin) {
return switch (position % 3) {
case 1: // Left
yield pageSize.getLowerLeftX() + margin;
case 2: // Center
yield (pageSize.getWidth() - actualWidth) / 2;
yield (pageSize.getWidth() - contentWidth) / 2;
case 0: // Right
yield pageSize.getUpperRightX() - actualWidth - margin;
yield pageSize.getUpperRightX() - contentWidth - margin;
default:
yield 0;
};
@@ -368,12 +509,12 @@ public class StampController {
private float calculatePositionY(
PDRectangle pageSize, int position, float height, float margin) {
return switch ((position - 1) / 3) {
case 0: // Top
yield pageSize.getUpperRightY() - height - margin;
case 1: // Middle
yield (pageSize.getHeight() - height) / 2;
case 2: // Bottom
yield pageSize.getLowerLeftY() + margin;
case 0: // Top - first line near the top
yield pageSize.getUpperRightY() - margin;
case 1: // Middle - center of text block at page center
yield (pageSize.getHeight() + height) / 2;
case 2: // Bottom - first line positioned so last line is at bottom margin
yield pageSize.getLowerLeftY() + margin + height;
default:
yield 0;
};
@@ -382,8 +523,4 @@ public class StampController {
private float calculateTextWidth(String text, PDFont font, float fontSize) throws IOException {
return font.getStringWidth(text) / 1000 * fontSize;
}
private float calculateTextCapHeight(PDFont font, float fontSize) {
return font.getFontDescriptor().getCapHeight() / 1000 * fontSize;
}
}
@@ -1,11 +1,9 @@
package stirling.software.SPDF.controller.api.pipeline;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystemException;
import java.nio.file.FileVisitOption;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -17,6 +15,7 @@ import java.time.LocalDate;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
@@ -24,7 +23,6 @@ import java.util.Map;
import java.util.Optional;
import java.util.stream.Stream;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.Resource;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
@@ -45,14 +43,20 @@ import stirling.software.common.util.FileMonitor;
@Slf4j
public class PipelineDirectoryProcessor {
private static final int MAX_DIRECTORY_DEPTH = 50; // Prevent excessive recursion
private final ObjectMapper objectMapper;
private final ApiDocService apiDocService;
private final PipelineProcessor processor;
private final FileMonitor fileMonitor;
private final PostHogService postHogService;
private final String watchedFoldersDir;
private final List<String> watchedFoldersDirs;
private final String finishedFoldersDir;
// Track processed directories in current scan to prevent duplicates
private final ThreadLocal<java.util.Set<Path>> processedDirsInScan =
ThreadLocal.withInitial(java.util.HashSet::new);
public PipelineDirectoryProcessor(
ObjectMapper objectMapper,
ApiDocService apiDocService,
@@ -65,13 +69,26 @@ public class PipelineDirectoryProcessor {
this.processor = processor;
this.fileMonitor = fileMonitor;
this.postHogService = postHogService;
this.watchedFoldersDir = runtimePathConfig.getPipelineWatchedFoldersPath();
this.watchedFoldersDirs = runtimePathConfig.getPipelineWatchedFoldersPaths();
this.finishedFoldersDir = runtimePathConfig.getPipelineFinishedFoldersPath();
}
@Scheduled(fixedRate = 60000)
public void scanFolders() {
Path watchedFolderPath = Paths.get(watchedFoldersDir).toAbsolutePath();
// Clear the processed directories set for this scan cycle
processedDirsInScan.get().clear();
try {
for (String watchedFoldersDir : watchedFoldersDirs) {
scanWatchedFolder(Paths.get(watchedFoldersDir).toAbsolutePath());
}
} finally {
// Clean up ThreadLocal to prevent memory leaks
processedDirsInScan.remove();
}
}
private void scanWatchedFolder(Path watchedFolderPath) {
if (!Files.exists(watchedFolderPath)) {
try {
Files.createDirectories(watchedFolderPath);
@@ -82,16 +99,34 @@ public class PipelineDirectoryProcessor {
}
}
// Validate the path is a directory and readable
if (!Files.isDirectory(watchedFolderPath)) {
log.error("Path is not a directory: {}", watchedFolderPath);
return;
}
if (!Files.isReadable(watchedFolderPath)) {
log.error("Directory is not readable: {}", watchedFolderPath);
return;
}
try {
// Use FOLLOW_LINKS to follow symlinks, with max depth to prevent infinite loops
Files.walkFileTree(
watchedFolderPath,
EnumSet.of(FileVisitOption.FOLLOW_LINKS),
MAX_DIRECTORY_DEPTH,
new SimpleFileVisitor<>() {
@Override
public FileVisitResult preVisitDirectory(
Path dir, BasicFileAttributes attrs) {
try {
String dirName =
dir.getFileName() != null
? dir.getFileName().toString()
: "";
// Skip root directory and "processing" subdirectories
if (!dir.equals(watchedFolderPath) && !dir.endsWith("processing")) {
if (!dir.equals(watchedFolderPath)
&& !"processing".equals(dirName)) {
handleDirectory(dir);
}
} catch (Exception e) {
@@ -102,8 +137,11 @@ public class PipelineDirectoryProcessor {
@Override
public FileVisitResult visitFileFailed(Path path, IOException exc) {
// Handle broken symlinks or inaccessible directories
log.error("Error accessing path: {}", path, exc);
// Handle broken symlinks, permission issues, or inaccessible
// directories
if (exc != null) {
log.debug("Cannot access path '{}': {}", path, exc.getMessage());
}
return FileVisitResult.CONTINUE;
}
});
@@ -113,6 +151,17 @@ public class PipelineDirectoryProcessor {
}
public void handleDirectory(Path dir) throws IOException {
// Normalize path to absolute to prevent duplicate processing from different path
// representations
Path normalizedDir = dir.toAbsolutePath().normalize();
// Check if we've already processed this directory in this scan cycle
java.util.Set<Path> processedDirs = processedDirsInScan.get();
if (!processedDirs.add(normalizedDir)) {
log.debug("Directory already processed in this scan cycle: {}", normalizedDir);
return;
}
log.info("Handling directory: {}", dir);
Path processingDir = createProcessingDirectory(dir);
Optional<Path> jsonFileOptional = findJsonFile(dir);
@@ -164,7 +213,8 @@ public class PipelineDirectoryProcessor {
postHogService.captureEvent("pipeline_directory_event", properties);
List<File> filesToProcess = prepareFilesForProcessing(files, processingDir);
runPipelineAgainstFiles(filesToProcess, config, dir, processingDir);
try (PipelineResult result =
runPipelineAgainstFiles(filesToProcess, config, dir, processingDir)) {}
}
}
@@ -303,14 +353,14 @@ public class PipelineDirectoryProcessor {
}
}
private void runPipelineAgainstFiles(
private PipelineResult runPipelineAgainstFiles(
List<File> filesToProcess, PipelineConfig config, Path dir, Path processingDir)
throws IOException {
try {
List<Resource> inputFiles =
processor.generateInputFiles(filesToProcess.toArray(new File[0]));
if (inputFiles == null || inputFiles.isEmpty()) {
return;
return new PipelineResult();
}
PipelineResult result = processor.runPipelineAgainstFiles(inputFiles, config);
@@ -321,9 +371,11 @@ public class PipelineDirectoryProcessor {
moveAndRenameFiles(result.getOutputFiles(), config, dir);
deleteOriginalFiles(filesToProcess, processingDir);
}
return result;
} catch (Exception e) {
log.error("Error during processing", e);
moveFilesBack(filesToProcess, processingDir);
return new PipelineResult();
}
}
@@ -350,8 +402,9 @@ public class PipelineDirectoryProcessor {
log.info("Created directory: {}", outputPath);
}
Path outputFile = outputPath.resolve(outputFileName);
try (OutputStream os = new FileOutputStream(outputFile.toFile())) {
os.write(((ByteArrayResource) resource).getByteArray());
try (OutputStream os = new FileOutputStream(outputFile.toFile());
InputStream is = resource.getInputStream()) {
is.transferTo(os);
}
log.info("File moved and renamed to {}", outputFile);
}
@@ -15,12 +15,13 @@ import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RequestCallback;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.multipart.MultipartFile;
@@ -38,6 +39,8 @@ import stirling.software.SPDF.model.PipelineResult;
import stirling.software.SPDF.service.ApiDocService;
import stirling.software.common.model.enumeration.Role;
import stirling.software.common.service.UserServiceInterface;
import stirling.software.common.util.TempFile;
import stirling.software.common.util.TempFileManager;
@Service
@Slf4j
@@ -49,13 +52,17 @@ public class PipelineProcessor {
private final ServletContext servletContext;
private final TempFileManager tempFileManager;
public PipelineProcessor(
ApiDocService apiDocService,
@Autowired(required = false) UserServiceInterface userService,
ServletContext servletContext) {
ServletContext servletContext,
TempFileManager tempFileManager) {
this.apiDocService = apiDocService;
this.userService = userService;
this.servletContext = servletContext;
this.tempFileManager = tempFileManager;
}
public static String removeTrailingNaming(String filename) {
@@ -137,14 +144,18 @@ public class PipelineProcessor {
body.add(entry.getKey(), entry.getValue());
}
}
ResponseEntity<byte[]> response = sendWebRequest(url, body);
ResponseEntity<Resource> response = sendWebRequest(url, body);
// If the operation is filter and the response body is null or empty,
// skip
// this
// file
if (response.getBody() instanceof TempFileResource tempFileResource) {
result.addTempFile(tempFileResource.getTempFile());
}
if (operation.startsWith("/api/v1/filter/filter-")
&& (response.getBody() == null
|| response.getBody().length == 0)) {
|| response.getBody().contentLength() == 0)) {
filtersApplied = true;
log.info("Skipping file due to filtering {}", operation);
continue;
@@ -154,7 +165,7 @@ public class PipelineProcessor {
hasErrors = true;
continue;
}
processOutputFiles(operation, response, newOutputFiles);
processOutputFiles(operation, response, newOutputFiles, result);
}
}
if (!hasInputFileType) {
@@ -215,10 +226,13 @@ public class PipelineProcessor {
body.add(entry.getKey(), entry.getValue());
}
}
ResponseEntity<byte[]> response = sendWebRequest(url, body);
ResponseEntity<Resource> response = sendWebRequest(url, body);
if (response.getBody() instanceof TempFileResource tempFileResource) {
result.addTempFile(tempFileResource.getTempFile());
}
// Handle the response
if (HttpStatus.OK.equals(response.getStatusCode())) {
processOutputFiles(operation, response, newOutputFiles);
processOutputFiles(operation, response, newOutputFiles, result);
} else {
// Log error if the response status is not OK
logPrintStream.println(
@@ -267,22 +281,47 @@ public class PipelineProcessor {
return result;
}
/* package */ ResponseEntity<byte[]> sendWebRequest(
/* package */ ResponseEntity<Resource> sendWebRequest(
String url, MultiValueMap<String, Object> body) {
RestTemplate restTemplate = new RestTemplate();
// Set up headers, including API key
HttpHeaders headers = new HttpHeaders();
String apiKey = getApiKeyForUser();
headers.add("X-API-KEY", apiKey);
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
// Create HttpEntity with the body and headers
if (apiKey != null && !apiKey.isEmpty()) {
headers.add("X-API-KEY", apiKey);
}
// Let the message converter set the multipart boundary/content type
HttpEntity<MultiValueMap<String, Object>> entity = new HttpEntity<>(body, headers);
// Make the request to the REST endpoint
return restTemplate.exchange(url, HttpMethod.POST, entity, byte[].class);
RequestCallback requestCallback =
restTemplate.httpEntityCallback(entity, Resource.class /* response type hint */);
return restTemplate.execute(
url,
HttpMethod.POST,
requestCallback,
response -> {
try {
TempFile tempFile = tempFileManager.createManagedTempFile("pipeline");
Files.copy(
response.getBody(),
tempFile.getPath(),
java.nio.file.StandardCopyOption.REPLACE_EXISTING);
TempFileResource resource = new TempFileResource(tempFile);
return ResponseEntity.status(response.getStatusCode())
.headers(response.getHeaders())
.body(resource);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
});
}
private List<Resource> processOutputFiles(
String operation, ResponseEntity<byte[]> response, List<Resource> newOutputFiles)
String operation,
ResponseEntity<Resource> response,
List<Resource> newOutputFiles,
PipelineResult result)
throws IOException {
// Define filename
String newFilename;
@@ -298,10 +337,14 @@ public class PipelineProcessor {
// Check if the response body is a zip file
if (isZip(response.getBody(), newFilename)) {
// Unzip the file and add all the files to the new output files
newOutputFiles.addAll(unzip(response.getBody()));
newOutputFiles.addAll(unzip(response.getBody(), result));
} else {
final Resource tempResource = response.getBody();
if (tempResource instanceof TempFileResource) {
result.addTempFile(((TempFileResource) tempResource).getTempFile());
}
Resource outputResource =
new ByteArrayResource(response.getBody()) {
new FileSystemResource(tempResource.getFile()) {
@Override
public String getFilename() {
@@ -313,7 +356,7 @@ public class PipelineProcessor {
return newOutputFiles;
}
public String extractFilename(ResponseEntity<byte[]> response) {
public String extractFilename(ResponseEntity<Resource> response) {
// Default filename if not found
String filename = "default-filename.ext";
HttpHeaders headers = response.getHeaders();
@@ -348,14 +391,7 @@ public class PipelineProcessor {
// debug statement
log.info("Reading file: {}", path);
if (Files.exists(path)) {
Resource fileResource =
new ByteArrayResource(Files.readAllBytes(path)) {
@Override
public String getFilename() {
return file.getName();
}
};
Resource fileResource = new FileSystemResource(file);
outputFiles.add(fileResource);
} else {
log.info("File not found: {}", path);
@@ -372,8 +408,11 @@ public class PipelineProcessor {
}
List<Resource> outputFiles = new ArrayList<>();
for (MultipartFile file : files) {
Path tempFile = Files.createTempFile("SPDF-upload-", ".tmp");
file.transferTo(tempFile);
Resource fileResource =
new ByteArrayResource(file.getBytes()) {
new FileSystemResource(tempFile.toFile()) {
@Override
public String getFilename() {
@@ -386,8 +425,8 @@ public class PipelineProcessor {
return outputFiles;
}
private boolean isZip(byte[] data, String filename) {
if (data == null || data.length < 4) {
private boolean isZip(Resource data, String filename) throws IOException {
if (data == null || data.contentLength() < 4) {
return false;
}
if (filename != null) {
@@ -398,29 +437,41 @@ public class PipelineProcessor {
}
}
// Check the first four bytes of the data against the standard zip magic number
return data[0] == 0x50 && data[1] == 0x4B && data[2] == 0x03 && data[3] == 0x04;
try (InputStream is = data.getInputStream()) {
byte[] header = new byte[4];
if (is.read(header) < 4) {
return false;
}
return header[0] == 0x50 && header[1] == 0x4B && header[2] == 0x03 && header[3] == 0x04;
}
}
private boolean isZip(byte[] data) {
private boolean isZip(Resource data) throws IOException {
return isZip(data, null);
}
private List<Resource> unzip(byte[] data) throws IOException {
log.info("Unzipping data of length: {}", data.length);
private List<Resource> unzip(Resource data, PipelineResult result) throws IOException {
log.info("Unzipping data of length: {}", data.contentLength());
List<Resource> unzippedFiles = new ArrayList<>();
try (ByteArrayInputStream bais = new ByteArrayInputStream(data);
try (InputStream bais = data.getInputStream();
ZipInputStream zis = ZipSecurity.createHardenedInputStream(bais)) {
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int count;
while ((count = zis.read(buffer)) != -1) {
baos.write(buffer, 0, count);
if (entry.isDirectory()) {
continue;
}
TempFile tempFile = tempFileManager.createManagedTempFile("unzip");
result.addTempFile(tempFile);
try (OutputStream os = Files.newOutputStream(tempFile.getPath())) {
byte[] buffer = new byte[4096];
int count;
while ((count = zis.read(buffer)) != -1) {
os.write(buffer, 0, count);
}
}
final String filename = entry.getName();
Resource fileResource =
new ByteArrayResource(baos.toByteArray()) {
new FileSystemResource(tempFile.getFile()) {
@Override
public String getFilename() {
@@ -428,9 +479,9 @@ public class PipelineProcessor {
}
};
// If the unzipped file is a zip file, unzip it
if (isZip(baos.toByteArray(), filename)) {
if (isZip(fileResource, filename)) {
log.info("File {} is a zip file. Unzipping...", filename);
unzippedFiles.addAll(unzip(baos.toByteArray()));
unzippedFiles.addAll(unzip(fileResource, result));
} else {
unzippedFiles.add(fileResource);
}
@@ -439,4 +490,17 @@ public class PipelineProcessor {
log.info("Unzipping completed. {} files were unzipped.", unzippedFiles.size());
return unzippedFiles;
}
private static class TempFileResource extends FileSystemResource {
private final TempFile tempFile;
public TempFileResource(TempFile tempFile) {
super(tempFile.getFile());
this.tempFile = tempFile;
}
public TempFile getTempFile() {
return tempFile;
}
}
}
@@ -3,21 +3,15 @@ package stirling.software.SPDF.controller.api.security;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.pdfbox.cos.COSInputStream;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.cos.COSString;
import org.apache.pdfbox.io.RandomAccessRead;
import org.apache.pdfbox.io.RandomAccessReadBufferedFile;
import org.apache.pdfbox.pdmodel.*;
import org.apache.pdfbox.pdmodel.common.PDMetadata;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
@@ -35,8 +29,7 @@ import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
import org.apache.pdfbox.pdmodel.graphics.optionalcontent.PDOptionalContentGroup;
import org.apache.pdfbox.pdmodel.graphics.optionalcontent.PDOptionalContentProperties;
import org.apache.pdfbox.pdmodel.interactive.action.PDActionJavaScript;
import org.apache.pdfbox.pdmodel.interactive.action.PDActionURI;
import org.apache.pdfbox.pdmodel.interactive.action.*;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationFileAttachment;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationLink;
@@ -44,23 +37,14 @@ import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDOutlin
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDOutlineNode;
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
import org.apache.pdfbox.pdmodel.interactive.form.PDField;
import org.apache.pdfbox.preflight.PreflightDocument;
import org.apache.pdfbox.preflight.ValidationResult;
import org.apache.pdfbox.preflight.exception.SyntaxValidationException;
import org.apache.pdfbox.preflight.exception.ValidationException;
import org.apache.pdfbox.preflight.parser.PreflightParser;
import org.apache.pdfbox.text.PDFTextStripper;
import org.apache.xmpbox.XMPMetadata;
import org.apache.xmpbox.schema.PDFAIdentificationSchema;
import org.apache.xmpbox.xml.DomXmpParser;
import org.apache.xmpbox.xml.XmpParsingException;
import org.apache.xmpbox.xml.XmpSerializer;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -68,21 +52,22 @@ import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.api.security.PDFVerificationResult;
import stirling.software.SPDF.service.VeraPDFService;
import stirling.software.common.annotations.AutoJobPostMapping;
import stirling.software.common.annotations.api.SecurityApi;
import stirling.software.common.model.api.PDFFile;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.RegexPatternUtils;
import stirling.software.common.util.WebResponseUtils;
@RestController
@RequestMapping("/api/v1/security")
@SecurityApi
@Slf4j
@Tag(name = "Security", description = "Security APIs")
@RequiredArgsConstructor
public class GetInfoOnPDF {
@@ -95,6 +80,7 @@ public class GetInfoOnPDF {
private static final ObjectMapper objectMapper = new ObjectMapper();
private final CustomPDFDocumentFactory pdfDocumentFactory;
private final VeraPDFService veraPDFService;
private static void addOutlinesToArray(PDOutlineItem outline, ArrayNode arrayNode) {
if (outline == null) return;
@@ -111,214 +97,8 @@ public class GetInfoOnPDF {
}
}
public static boolean checkForStandard(PDDocument document, String standardKeyword) {
if ("PDF/A".equalsIgnoreCase(standardKeyword)) {
return getPdfAConformanceLevel(document) != null;
}
return checkStandardInMetadata(document, standardKeyword);
}
public static String getPdfAConformanceLevel(PDDocument document) {
if (document == null || document.isEncrypted()) {
return null;
}
return getPdfAVersionFromMetadata(document);
}
private static String getPdfAVersionFromMetadata(PDDocument document) {
try {
PDMetadata pdMetadata = document.getDocumentCatalog().getMetadata();
if (pdMetadata != null) {
try (COSInputStream metaStream = pdMetadata.createInputStream()) {
DomXmpParser domXmpParser = new DomXmpParser();
XMPMetadata xmpMeta = domXmpParser.parse(metaStream);
PDFAIdentificationSchema pdfId = xmpMeta.getPDFAIdentificationSchema();
if (pdfId != null) {
Integer part = pdfId.getPart();
String conformance = pdfId.getConformance();
if (part != null && conformance != null) {
return part + conformance.toUpperCase(Locale.ROOT);
}
} else {
try (COSInputStream rawStream = pdMetadata.createInputStream()) {
byte[] metadataBytes = rawStream.readAllBytes();
String rawMetadata = new String(metadataBytes, StandardCharsets.UTF_8);
String extracted = extractPdfAVersionFromRawXml(rawMetadata);
if (extracted != null) {
return extracted;
}
}
}
} catch (XmpParsingException e) {
log.debug("XMP parsing failed, trying raw metadata search: {}", e.getMessage());
try (COSInputStream metaStream = pdMetadata.createInputStream()) {
byte[] metadataBytes = metaStream.readAllBytes();
String rawMetadata = new String(metadataBytes, StandardCharsets.UTF_8);
String extracted = extractPdfAVersionFromRawXml(rawMetadata);
if (extracted != null) {
return extracted;
}
}
}
}
} catch (Exception e) {
log.debug("Error reading PDF/A metadata: {}", e.getMessage());
}
return null;
}
private static String extractPdfAVersionFromRawXml(String rawXml) {
if (rawXml == null || rawXml.isEmpty()) {
return null;
}
try {
Pattern partPattern = RegexPatternUtils.getInstance().getPdfAidPartPattern();
Pattern confPattern = RegexPatternUtils.getInstance().getPdfAidConformancePattern();
Matcher partMatcher = partPattern.matcher(rawXml);
Matcher confMatcher = confPattern.matcher(rawXml);
if (partMatcher.find() && confMatcher.find()) {
String part = partMatcher.group(1);
String conformance = confMatcher.group(1).toUpperCase(Locale.ROOT);
return part + conformance;
}
} catch (Exception e) {
log.debug("Error parsing raw XMP for PDF/A version: {}", e.getMessage());
}
return null;
}
private static boolean validatePdfAWithPreflight(PDDocument document, String version) {
if (document == null || document.isEncrypted()) {
return false;
}
// Use Stream-to-File pattern: save to temp file instead of loading into memory
// This prevents OutOfMemoryError on large PDFs
Path tempFile = null;
try {
tempFile = Files.createTempFile("preflight-", ".pdf");
// Save document to temp file (avoids loading entire document into memory)
try (var outputStream = Files.newOutputStream(tempFile)) {
document.save(outputStream);
}
// Use RandomAccessReadBufferedFile for efficient file-based reading
// This avoids Windows file locking issues that occur with memory-mapped files
try (RandomAccessRead source = new RandomAccessReadBufferedFile(tempFile.toFile())) {
PreflightParser parser = new PreflightParser(source);
try (PDDocument parsedDocument = parser.parse()) {
if (!(parsedDocument instanceof PreflightDocument preflightDocument)) {
log.debug(
"Parsed document is not a PreflightDocument; unable to validate claimed PDF/A {}",
version);
return false;
}
try {
ValidationResult result = preflightDocument.validate();
if (!result.isValid() && log.isDebugEnabled()) {
log.debug(
"PDF/A validation found {} errors for claimed version {}",
result.getErrorsList().size(),
version);
int logged = 0;
for (ValidationResult.ValidationError error : result.getErrorsList()) {
log.debug(
" Error {}: {}", error.getErrorCode(), error.getDetails());
if (++logged >= MAX_LOGGED_ERRORS) {
break;
}
}
}
return result.isValid();
} catch (ValidationException e) {
log.debug(
"Validation exception during PDF/A validation: {}", e.getMessage());
}
} catch (SyntaxValidationException e) {
log.debug(
"Syntax validation failed during PDF/A validation: {}", e.getMessage());
return false;
}
}
} catch (IOException e) {
log.debug("IOException during PDF/A validation: {}", e.getMessage());
} catch (Exception e) {
log.debug("Unexpected error during PDF/A validation: {}", e.getMessage());
} finally {
// Explicitly clean up temp file to prevent disk exhaustion
// This must be in finally block to ensure cleanup even on exceptions
if (tempFile != null) {
try {
Files.deleteIfExists(tempFile);
} catch (IOException e) {
log.warn(
"Failed to delete temp file during PDF/A validation cleanup: {}",
tempFile,
e);
}
}
}
return false;
}
private static boolean checkStandardInMetadata(PDDocument document, String standardKeyword) {
// Check XMP Metadata
try {
PDMetadata pdMetadata = document.getDocumentCatalog().getMetadata();
if (pdMetadata != null) {
try (COSInputStream metaStream = pdMetadata.createInputStream()) {
// First try to read raw metadata as string to check for standard keywords
byte[] metadataBytes = metaStream.readAllBytes();
String rawMetadata = new String(metadataBytes, StandardCharsets.UTF_8);
if (rawMetadata.contains(standardKeyword)) {
return true;
}
}
// If raw check doesn't find it, try parsing with XMP parser
try (COSInputStream metaStream = pdMetadata.createInputStream()) {
try {
DomXmpParser domXmpParser = new DomXmpParser();
XMPMetadata xmpMeta = domXmpParser.parse(metaStream);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
new XmpSerializer().serialize(xmpMeta, baos, true);
String xmpString = baos.toString(StandardCharsets.UTF_8);
if (xmpString.contains(standardKeyword)) {
return true;
}
} catch (XmpParsingException e) {
// XMP parsing failed, but we already checked raw metadata above
log.debug(
"XMP parsing failed for standard check, but raw metadata was already checked: {}",
e.getMessage());
}
}
}
} catch (Exception e) {
ExceptionUtils.logException("PDF standard checking", e);
}
return false;
}
private static ObjectNode generatePDFSummaryData(
PDDocument document, String pdfaConformanceLevel, Boolean pdfaValidationPassed) {
PDDocument document, List<PDFVerificationResult> verificationResults) {
ObjectNode summaryData = objectMapper.createObjectNode();
// Check if encrypted
@@ -346,24 +126,16 @@ public class GetInfoOnPDF {
}
// Check standard compliance
if (pdfaConformanceLevel != null) {
summaryData.put("standardCompliance", "PDF/A-" + pdfaConformanceLevel);
summaryData.put("standardPurpose", "long-term archiving");
if (pdfaValidationPassed != null) {
summaryData.put("standardValidationPassed", pdfaValidationPassed);
if (verificationResults != null && !verificationResults.isEmpty()) {
ArrayNode complianceArray = objectMapper.createArrayNode();
for (PDFVerificationResult result : verificationResults) {
ObjectNode complianceNode = objectMapper.createObjectNode();
complianceNode.put("Standard", result.getStandard());
complianceNode.put("Compliant", result.isCompliant());
complianceNode.put("Summary", result.getComplianceSummary());
complianceArray.add(complianceNode);
}
} else if (checkForStandard(document, "PDF/X")) {
summaryData.put("standardCompliance", "PDF/X");
summaryData.put("standardPurpose", "graphic exchange");
} else if (checkForStandard(document, "PDF/UA")) {
summaryData.put("standardCompliance", "PDF/UA");
summaryData.put("standardPurpose", "universal accessibility");
} else if (checkForStandard(document, "PDF/E")) {
summaryData.put("standardCompliance", "PDF/E");
summaryData.put("standardPurpose", "engineering workflows");
} else if (checkForStandard(document, "PDF/VT")) {
summaryData.put("standardCompliance", "PDF/VT");
summaryData.put("standardPurpose", "variable and transactional printing");
summaryData.set("Compliance", complianceArray);
}
return summaryData;
@@ -591,40 +363,113 @@ public class GetInfoOnPDF {
return docInfoNode;
}
private static ObjectNode extractComplianceInfo(PDDocument document) {
private static ObjectNode extractComplianceInfo(
PDDocument doc, List<PDFVerificationResult> verificationResults) {
ObjectNode compliancy = objectMapper.createObjectNode();
try {
String pdfaConformanceLevel = getPdfAConformanceLevel(document);
boolean isPdfACompliant = pdfaConformanceLevel != null;
boolean isPdfXCompliant = checkForStandard(document, "PDF/X");
boolean isPdfECompliant = checkForStandard(document, "PDF/E");
boolean isPdfVTCompliant = checkForStandard(document, "PDF/VT");
boolean isPdfUACompliant = checkForStandard(document, "PDF/UA");
boolean isPdfBCompliant = checkForStandard(document, "PDF/B");
boolean isPdfSECCompliant = checkForStandard(document, "PDF/SEC");
boolean isPdfA = false;
boolean isPdfUA = false;
boolean isPdfX = false;
boolean isPdfE = false;
boolean isPdfB = false;
String pdfAConformanceLevel = null;
compliancy.put("IsPDF/ACompliant", isPdfACompliant);
if (pdfaConformanceLevel != null) {
compliancy.put("PDF/AConformanceLevel", pdfaConformanceLevel);
Boolean pdfaValidationPassed =
validatePdfAWithPreflight(document, pdfaConformanceLevel);
compliancy.put("IsPDF/AValidated", pdfaValidationPassed);
if (verificationResults != null) {
for (PDFVerificationResult result : verificationResults) {
if (result == null) continue;
if (result.isCompliant()) {
String std = result.getStandard().toLowerCase();
if (std.contains("pdf_a") || std.contains("pdfa")) {
isPdfA = true;
String profile = result.getValidationProfile();
if (profile != null) {
if (profile.contains("1b")
|| profile.contains("2b")
|| profile.contains("3b")) {
isPdfB = true;
}
// Simple extraction: remove "pdfa-" prefix
pdfAConformanceLevel = profile.replace("pdfa-", "");
}
}
if (std.contains("pdf_ua") || std.contains("pdfua")) isPdfUA = true;
if (std.contains("pdf_x") || std.contains("pdfx")) isPdfX = true;
if (std.contains("pdf_e") || std.contains("pdfe")) isPdfE = true;
}
}
compliancy.put("IsPDF/XCompliant", isPdfXCompliant);
compliancy.put("IsPDF/ECompliant", isPdfECompliant);
compliancy.put("IsPDF/VTCompliant", isPdfVTCompliant);
compliancy.put("IsPDF/UACompliant", isPdfUACompliant);
compliancy.put("IsPDF/BCompliant", isPdfBCompliant);
compliancy.put("IsPDF/SECCompliant", isPdfSECCompliant);
}
} catch (Exception e) {
log.error("Error extracting compliance info: {}", e.getMessage());
compliancy.put("IsPDF/ACompliant", isPdfA);
compliancy.put("IsPDF/UACompliant", isPdfUA);
compliancy.put("IsPDF/ECompliant", isPdfE);
compliancy.put("IsPDF/VTCompliant", false); // Not currently implemented
compliancy.put("IsPDF/BCompliant", isPdfB);
if (pdfAConformanceLevel != null) {
compliancy.put("PDF/AConformanceLevel", pdfAConformanceLevel);
}
compliancy.put("IsPDF/SECCompliant", isSECCompliant(doc));
if (verificationResults != null && !verificationResults.isEmpty()) {
// Keep original simple structure as backup or extra info
for (PDFVerificationResult result : verificationResults) {
if (result == null) continue;
String standard = result.getStandard();
boolean isCompliant = result.isCompliant();
if (standard != null) {
// Avoid overwriting specific keys if collision, but here keys are distinct
// enough
compliancy.put(standard, isCompliant);
}
}
}
return compliancy;
}
private static boolean isSECCompliant(PDDocument doc) {
try {
// 1. Check Encryption
if (doc.isEncrypted()) return false;
PDDocumentCatalog catalog = doc.getDocumentCatalog();
// 2. Check for JavaScript (Active Content)
if (catalog.getOpenAction() instanceof PDActionJavaScript) return false;
if (catalog.getNames() != null && catalog.getNames().getJavaScript() != null)
return false;
// Check for AcroForm
if (catalog.getAcroForm() != null) return false;
// 3. Check for Embedded Files
if (catalog.getNames() != null && catalog.getNames().getEmbeddedFiles() != null)
return false;
// 4. Check for External Links or Navigation Actions
for (PDPage page : doc.getPages()) {
for (PDAnnotation annotation : page.getAnnotations()) {
if (annotation instanceof PDAnnotationLink) {
PDAnnotationLink link = (PDAnnotationLink) annotation;
PDAction action = link.getAction();
if (action instanceof PDActionURI
|| action instanceof PDActionLaunch
|| action instanceof PDActionRemoteGoTo
|| action instanceof PDActionSubmitForm) {
return false;
}
}
}
}
return true;
} catch (Exception e) {
log.error("Error checking SEC compliance: {}", e.getMessage());
return false;
}
}
private static ObjectNode extractEncryptionInfo(PDDocument document) {
ObjectNode encryption = objectMapper.createObjectNode();
@@ -1215,7 +1060,7 @@ public class GetInfoOnPDF {
return stats;
}
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/get-info-on-pdf")
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/get-info-on-pdf")
@Operation(
summary = "Get comprehensive PDF information",
description =
@@ -1231,6 +1076,13 @@ public class GetInfoOnPDF {
return createErrorResponse("Invalid PDF file: " + e.getMessage());
}
List<PDFVerificationResult> verificationResults = null;
try {
verificationResults = veraPDFService.validatePDF(inputFile.getInputStream());
} catch (Exception e) {
log.error("VeraPDF validation failed", e);
}
boolean readonly = true;
try (PDDocument pdfBoxDoc = pdfDocumentFactory.load(inputFile, readonly)) {
@@ -1239,20 +1091,13 @@ public class GetInfoOnPDF {
ObjectNode metadata = extractMetadata(pdfBoxDoc);
ObjectNode basicInfo = extractBasicInfo(pdfBoxDoc, inputFile.getSize());
ObjectNode docInfoNode = extractDocumentInfo(pdfBoxDoc);
ObjectNode compliancy = extractComplianceInfo(pdfBoxDoc);
ObjectNode compliancy = extractComplianceInfo(pdfBoxDoc, verificationResults);
ObjectNode encryption = extractEncryptionInfo(pdfBoxDoc);
ObjectNode permissionsNode = extractPermissions(pdfBoxDoc);
ObjectNode other = extractOtherInfo(pdfBoxDoc);
ObjectNode formFieldsNode = extractFormFields(pdfBoxDoc);
// Generate summary data
String pdfaConformanceLevel = getPdfAConformanceLevel(pdfBoxDoc);
Boolean pdfaValidationPassed = null;
if (pdfaConformanceLevel != null) {
pdfaValidationPassed = validatePdfAWithPreflight(pdfBoxDoc, pdfaConformanceLevel);
}
ObjectNode summaryData =
generatePDFSummaryData(pdfBoxDoc, pdfaConformanceLevel, pdfaValidationPassed);
ObjectNode summaryData = generatePDFSummaryData(pdfBoxDoc, verificationResults);
// Extract per-page information
ObjectNode pageInfoParent = extractPerPageInfo(pdfBoxDoc);
@@ -6,16 +6,12 @@ import java.util.List;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import org.verapdf.core.EncryptedPdfException;
import org.verapdf.core.ModelParsingException;
import org.verapdf.core.ValidationException;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -23,11 +19,11 @@ import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.api.security.PDFVerificationRequest;
import stirling.software.SPDF.model.api.security.PDFVerificationResult;
import stirling.software.SPDF.service.VeraPDFService;
import stirling.software.common.annotations.AutoJobPostMapping;
import stirling.software.common.annotations.api.SecurityApi;
import stirling.software.common.util.ExceptionUtils;
@RestController
@RequestMapping("/api/v1/security")
@Tag(name = "Security", description = "Security APIs")
@SecurityApi
@RequiredArgsConstructor
@Slf4j
public class VerifyPDFController {
@@ -41,7 +37,7 @@ public class VerifyPDFController {
+ "Automatically detects PDF/A, PDF/UA-1, PDF/UA-2, and WTPDF standards "
+ "from the document's XMP metadata and validates compliance. "
+ "Input:PDF Output:JSON Type:SISO")
@PostMapping(value = "/verify-pdf", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@AutoJobPostMapping(value = "/verify-pdf", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<List<PDFVerificationResult>> verifyPDF(
@ModelAttribute PDFVerificationRequest request) {
@@ -1,221 +0,0 @@
package stirling.software.SPDF.controller.web;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.servlet.ModelAndView;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.config.EndpointConfiguration;
import stirling.software.common.model.ApplicationProperties;
import stirling.software.common.util.ApplicationContextProvider;
import stirling.software.common.util.CheckProgramInstall;
@Controller
@Tag(name = "Convert", description = "Convert APIs")
public class ConverterWebController {
@GetMapping("/img-to-pdf")
@Hidden
public String convertImgToPdfForm(Model model) {
model.addAttribute("currentPage", "img-to-pdf");
return "convert/img-to-pdf";
}
@GetMapping("/cbz-to-pdf")
@Hidden
public String convertCbzToPdfForm(Model model) {
model.addAttribute("currentPage", "cbz-to-pdf");
return "convert/cbz-to-pdf";
}
@GetMapping("/pdf-to-cbz")
@Hidden
public String convertPdfToCbzForm(Model model) {
model.addAttribute("currentPage", "pdf-to-cbz");
return "convert/pdf-to-cbz";
}
@GetMapping("/cbr-to-pdf")
@Hidden
public String convertCbrToPdfForm(Model model) {
model.addAttribute("currentPage", "cbr-to-pdf");
return "convert/cbr-to-pdf";
}
@GetMapping("/ebook-to-pdf")
@Hidden
public String convertEbookToPdfForm(Model model) {
model.addAttribute("currentPage", "ebook-to-pdf");
return "convert/ebook-to-pdf";
}
@GetMapping("/pdf-to-epub")
@Hidden
public String convertPdfToEpubForm(Model model) {
if (!ApplicationContextProvider.getBean(EndpointConfiguration.class)
.isEndpointEnabled("pdf-to-epub")) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
}
model.addAttribute("currentPage", "pdf-to-epub");
return "convert/pdf-to-epub";
}
@GetMapping("/pdf-to-cbr")
@Hidden
public String convertPdfToCbrForm(Model model) {
if (!ApplicationContextProvider.getBean(EndpointConfiguration.class)
.isEndpointEnabled("pdf-to-cbr")) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
}
model.addAttribute("currentPage", "pdf-to-cbr");
return "convert/pdf-to-cbr";
}
@GetMapping("/html-to-pdf")
@Hidden
public String convertHTMLToPdfForm(Model model) {
model.addAttribute("currentPage", "html-to-pdf");
return "convert/html-to-pdf";
}
@GetMapping("/markdown-to-pdf")
@Hidden
public String convertMarkdownToPdfForm(Model model) {
model.addAttribute("currentPage", "markdown-to-pdf");
return "convert/markdown-to-pdf";
}
@GetMapping("/pdf-to-markdown")
@Hidden
public String convertPdfToMarkdownForm(Model model) {
model.addAttribute("currentPage", "pdf-to-markdown");
return "convert/pdf-to-markdown";
}
@GetMapping("/url-to-pdf")
@Hidden
public String convertURLToPdfForm(Model model) {
model.addAttribute("currentPage", "url-to-pdf");
return "convert/url-to-pdf";
}
@GetMapping("/file-to-pdf")
@Hidden
public String convertToPdfForm(Model model) {
model.addAttribute("currentPage", "file-to-pdf");
return "convert/file-to-pdf";
}
// PDF TO......
@GetMapping("/pdf-to-img")
@Hidden
public String pdfToimgForm(Model model) {
boolean isPython = CheckProgramInstall.isPythonAvailable();
ApplicationProperties properties =
ApplicationContextProvider.getBean(ApplicationProperties.class);
if (properties != null && properties.getSystem() != null) {
model.addAttribute("maxDPI", properties.getSystem().getMaxDPI());
} else {
model.addAttribute("maxDPI", 500); // Default value if not set
}
model.addAttribute("isPython", isPython);
model.addAttribute("currentPage", "pdf-to-img");
return "convert/pdf-to-img";
}
@GetMapping("/pdf-to-html")
@Hidden
public ModelAndView pdfToHTML() {
ModelAndView modelAndView = new ModelAndView("convert/pdf-to-html");
modelAndView.addObject("currentPage", "pdf-to-html");
return modelAndView;
}
@GetMapping("/pdf-to-presentation")
@Hidden
public ModelAndView pdfToPresentation() {
ModelAndView modelAndView = new ModelAndView("convert/pdf-to-presentation");
modelAndView.addObject("currentPage", "pdf-to-presentation");
return modelAndView;
}
@GetMapping("/pdf-to-text")
@Hidden
public ModelAndView pdfToText() {
ModelAndView modelAndView = new ModelAndView("convert/pdf-to-text");
modelAndView.addObject("currentPage", "pdf-to-text");
return modelAndView;
}
@GetMapping("/pdf-to-word")
@Hidden
public ModelAndView pdfToWord() {
ModelAndView modelAndView = new ModelAndView("convert/pdf-to-word");
modelAndView.addObject("currentPage", "pdf-to-word");
return modelAndView;
}
@GetMapping("/pdf-to-xml")
@Hidden
public ModelAndView pdfToXML() {
ModelAndView modelAndView = new ModelAndView("convert/pdf-to-xml");
modelAndView.addObject("currentPage", "pdf-to-xml");
return modelAndView;
}
@GetMapping("/pdf-to-csv")
@Hidden
public ModelAndView pdfToCSV() {
ModelAndView modelAndView = new ModelAndView("convert/pdf-to-csv");
modelAndView.addObject("currentPage", "pdf-to-csv");
return modelAndView;
}
@GetMapping("/pdf-to-pdfa")
@Hidden
public String pdfToPdfAForm(Model model) {
model.addAttribute("currentPage", "pdf-to-pdfa");
return "convert/pdf-to-pdfa";
}
@GetMapping("/pdf-to-vector")
@Hidden
public String pdfToVectorForm(Model model) {
model.addAttribute("currentPage", "pdf-to-vector");
return "convert/pdf-to-vector";
}
@GetMapping("/vector-to-pdf")
@Hidden
public String vectorToPdfForm(Model model) {
model.addAttribute("currentPage", "vector-to-pdf");
return "convert/vector-to-pdf";
}
@GetMapping("/eml-to-pdf")
@Hidden
public String convertEmlToPdfForm(Model model) {
model.addAttribute("currentPage", "eml-to-pdf");
return "convert/eml-to-pdf";
}
@GetMapping("/pdf-to-video")
@Hidden
public String pdfToVideo(Model model) {
ApplicationProperties properties =
ApplicationContextProvider.getBean(ApplicationProperties.class);
if (properties != null && properties.getSystem() != null) {
model.addAttribute("maxDPI", properties.getSystem().getMaxDPI());
} else {
model.addAttribute("maxDPI", 500);
}
model.addAttribute("currentPage", "pdf-to-video");
return "convert/pdf-to-video";
}
}
@@ -1,352 +0,0 @@
package stirling.software.SPDF.controller.web;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.stream.Stream;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.ui.Model;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.SignatureFile;
import stirling.software.SPDF.service.SharedSignatureService;
import stirling.software.common.configuration.InstallationPathConfig;
import stirling.software.common.configuration.RuntimePathConfig;
import stirling.software.common.service.UserServiceInterface;
import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.GeneralUtils;
// @Controller // Disabled - Backend-only mode, no Thymeleaf UI
@Tag(name = "General", description = "General APIs")
@Slf4j
public class GeneralWebController {
private final SharedSignatureService signatureService;
private final UserServiceInterface userService;
private final ResourceLoader resourceLoader;
private final RuntimePathConfig runtimePathConfig;
public GeneralWebController(
SharedSignatureService signatureService,
@Autowired(required = false) UserServiceInterface userService,
ResourceLoader resourceLoader,
RuntimePathConfig runtimePathConfig) {
this.signatureService = signatureService;
this.userService = userService;
this.resourceLoader = resourceLoader;
this.runtimePathConfig = runtimePathConfig;
}
@Deprecated
// @GetMapping("/pipeline")
@Hidden
public String pipelineForm(Model model) {
model.addAttribute("currentPage", "pipeline");
List<String> pipelineConfigs = new ArrayList<>();
List<Map<String, String>> pipelineConfigsWithNames = new ArrayList<>();
if (new File(runtimePathConfig.getPipelineDefaultWebUiConfigs()).exists()) {
try (Stream<Path> paths =
Files.walk(Paths.get(runtimePathConfig.getPipelineDefaultWebUiConfigs()))) {
List<Path> jsonFiles =
paths.filter(Files::isRegularFile)
.filter(p -> p.toString().endsWith(".json"))
.toList();
for (Path jsonFile : jsonFiles) {
String content = Files.readString(jsonFile, StandardCharsets.UTF_8);
pipelineConfigs.add(content);
}
for (String config : pipelineConfigs) {
Map<String, Object> jsonContent =
new ObjectMapper()
.readValue(config, new TypeReference<Map<String, Object>>() {});
String name = (String) jsonContent.get("name");
if (name == null || name.isEmpty()) {
String filename =
jsonFiles
.get(pipelineConfigs.indexOf(config))
.getFileName()
.toString();
name = filename.substring(0, filename.lastIndexOf('.'));
}
Map<String, String> configWithName = new HashMap<>();
configWithName.put("json", config);
configWithName.put("name", name);
pipelineConfigsWithNames.add(configWithName);
}
} catch (IOException e) {
log.error("exception", e);
}
}
if (pipelineConfigsWithNames.isEmpty()) {
Map<String, String> configWithName = new HashMap<>();
configWithName.put("json", "");
configWithName.put("name", "No preloaded configs found");
pipelineConfigsWithNames.add(configWithName);
}
model.addAttribute("pipelineConfigsWithNames", pipelineConfigsWithNames);
model.addAttribute("pipelineConfigs", pipelineConfigs);
return "pipeline";
}
@Deprecated
// @GetMapping("/merge-pdfs")
@Hidden
public String mergePdfForm(Model model) {
model.addAttribute("currentPage", "merge-pdfs");
return "merge-pdfs";
}
@Deprecated
// @GetMapping("/split-pdf-by-sections")
@Hidden
public String splitPdfBySections(Model model) {
model.addAttribute("currentPage", "split-pdf-by-sections");
return "split-pdf-by-sections";
}
@Deprecated
// @GetMapping("/split-pdf-by-chapters")
@Hidden
public String splitPdfByChapters(Model model) {
model.addAttribute("currentPage", "split-pdf-by-chapters");
return "split-pdf-by-chapters";
}
@Deprecated
// @GetMapping("/view-pdf")
@Hidden
public String ViewPdfForm2(Model model) {
model.addAttribute("currentPage", "view-pdf");
return "view-pdf";
}
@Deprecated
// @GetMapping("/edit-table-of-contents")
@Hidden
public String editTableOfContents(Model model) {
model.addAttribute("currentPage", "edit-table-of-contents");
return "edit-table-of-contents";
}
@Deprecated
// @GetMapping("/multi-tool")
@Hidden
public String multiToolForm(Model model) {
model.addAttribute("currentPage", "multi-tool");
return "multi-tool";
}
@Deprecated
// @GetMapping("/remove-pages")
@Hidden
public String pageDeleter(Model model) {
model.addAttribute("currentPage", "remove-pages");
return "remove-pages";
}
@Deprecated
// @GetMapping("/pdf-organizer")
@Hidden
public String pageOrganizer(Model model) {
model.addAttribute("currentPage", "pdf-organizer");
return "pdf-organizer";
}
@Deprecated
// @GetMapping("/extract-page")
@Hidden
public String extractPages(Model model) {
model.addAttribute("currentPage", "extract-page");
return "extract-page";
}
@Deprecated
// @GetMapping("/pdf-to-single-page")
@Hidden
public String pdfToSinglePage(Model model) {
model.addAttribute("currentPage", "pdf-to-single-page");
return "pdf-to-single-page";
}
@Deprecated
// @GetMapping("/rotate-pdf")
@Hidden
public String rotatePdfForm(Model model) {
model.addAttribute("currentPage", "rotate-pdf");
return "rotate-pdf";
}
@Deprecated
// @GetMapping("/split-pdfs")
@Hidden
public String splitPdfForm(Model model) {
model.addAttribute("currentPage", "split-pdfs");
return "split-pdfs";
}
@Deprecated
// @GetMapping("/sign")
@Hidden
public String signForm(Model model) {
String username = "";
if (userService != null) {
username = userService.getCurrentUsername();
}
// Get signatures from both personal and ALL_USERS folders
List<SignatureFile> signatures = signatureService.getAvailableSignatures(username);
model.addAttribute("currentPage", "sign");
model.addAttribute("fonts", getFontNames());
model.addAttribute("signatures", signatures);
return "sign";
}
@Deprecated
// @GetMapping("/multi-page-layout")
@Hidden
public String multiPageLayoutForm(Model model) {
model.addAttribute("currentPage", "multi-page-layout");
return "multi-page-layout";
}
@Deprecated
// @GetMapping("/scale-pages")
@Hidden
public String scalePagesFrom(Model model) {
model.addAttribute("currentPage", "scale-pages");
return "scale-pages";
}
@Deprecated
// @GetMapping("/split-by-size-or-count")
@Hidden
public String splitBySizeOrCount(Model model) {
model.addAttribute("currentPage", "split-by-size-or-count");
return "split-by-size-or-count";
}
@Deprecated
// @GetMapping("/overlay-pdf")
@Hidden
public String overlayPdf(Model model) {
model.addAttribute("currentPage", "overlay-pdf");
return "overlay-pdf";
}
private List<FontResource> getFontNames() {
List<FontResource> fontNames = new ArrayList<>();
// Extract font names from classpath
fontNames.addAll(getFontNamesFromLocation("classpath:static/fonts/*.woff2"));
// Extract font names from external directory
fontNames.addAll(
getFontNamesFromLocation(
"file:"
+ InstallationPathConfig.getStaticPath()
+ "fonts"
+ File.separator
+ "*"));
return fontNames;
}
private List<FontResource> getFontNamesFromLocation(String locationPattern) {
try {
Resource[] resources =
GeneralUtils.getResourcesFromLocationPattern(locationPattern, resourceLoader);
return Arrays.stream(resources)
.map(
resource -> {
try {
String filename = resource.getFilename();
if (filename != null) {
int lastDotIndex = filename.lastIndexOf('.');
if (lastDotIndex != -1) {
String name = filename.substring(0, lastDotIndex);
String extension = filename.substring(lastDotIndex + 1);
return new FontResource(name, extension);
}
}
return null;
} catch (Exception e) {
throw ExceptionUtils.createRuntimeException(
"error.fontLoadingFailed",
"Error processing font file",
e);
}
})
.filter(Objects::nonNull)
.toList();
} catch (Exception e) {
throw ExceptionUtils.createRuntimeException(
"error.fontDirectoryReadFailed", "Failed to read font directory", e);
}
}
public String getFormatFromExtension(String extension) {
return switch (extension) {
case "ttf" -> "truetype";
case "woff" -> "woff";
case "woff2" -> "woff2";
case "eot" -> "embedded-opentype";
case "svg" -> "svg";
default ->
// or throw an exception if an unexpected extension is encountered
"";
};
}
@Deprecated
// @GetMapping("/crop")
@Hidden
public String cropForm(Model model) {
model.addAttribute("currentPage", "crop");
return "crop";
}
@Deprecated
// @GetMapping("/auto-split-pdf")
@Hidden
public String autoSPlitPDFForm(Model model) {
model.addAttribute("currentPage", "auto-split-pdf");
return "auto-split-pdf";
}
@Deprecated
// @GetMapping("/remove-image-pdf")
@Hidden
public String removeImagePdfForm(Model model) {
model.addAttribute("currentPage", "remove-image-pdf");
return "remove-image-pdf";
}
@Setter
@Getter
public class FontResource {
private String name;
private String extension;
private String type;
public FontResource(String name, String extension) {
this.name = name;
this.extension = extension;
this.type = getFormatFromExtension(extension);
}
}
}
@@ -1,98 +0,0 @@
package stirling.software.SPDF.controller.web;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.http.MediaType;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.annotations.Hidden;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.Dependency;
import stirling.software.common.model.ApplicationProperties;
@Slf4j
// @Controller // Disabled - Backend-only mode, no Thymeleaf UI
@RequiredArgsConstructor
public class HomeWebController {
private final ApplicationProperties applicationProperties;
@Deprecated
// @GetMapping("/about")
@Hidden
public String gameForm(Model model) {
model.addAttribute("currentPage", "about");
return "about";
}
@Deprecated
// @GetMapping("/licenses")
@Hidden
public String licensesForm(Model model) {
model.addAttribute("currentPage", "licenses");
Resource resource = new ClassPathResource("static/3rdPartyLicenses.json");
try (InputStream is = resource.getInputStream()) {
String json = new String(is.readAllBytes(), StandardCharsets.UTF_8);
ObjectMapper mapper = new ObjectMapper();
Map<String, List<Dependency>> data = mapper.readValue(json, new TypeReference<>() {});
model.addAttribute("dependencies", data.get("dependencies"));
} catch (IOException e) {
log.error("exception", e);
}
return "licenses";
}
@Deprecated
// @GetMapping("/releases")
public String getReleaseNotes(Model model) {
return "releases";
}
@Deprecated
// @GetMapping("/")
public String home(Model model) {
model.addAttribute("currentPage", "home");
String showSurvey = System.getenv("SHOW_SURVEY");
boolean showSurveyValue = showSurvey == null || "true".equalsIgnoreCase(showSurvey);
model.addAttribute("showSurveyFromDocker", showSurveyValue);
return "home";
}
@Deprecated
// @GetMapping("/home")
public String root(Model model) {
return "redirect:/";
}
@Deprecated
// @GetMapping("/home-legacy")
public String redirectHomeLegacy() {
return "redirect:/";
}
@GetMapping(value = "/robots.txt", produces = MediaType.TEXT_PLAIN_VALUE)
@ResponseBody
@Hidden
public String getRobotsTxt() {
boolean allowGoogle = applicationProperties.getSystem().isGooglevisibility();
if (allowGoogle) {
return "User-agent: Googlebot\nAllow: /\n\nUser-agent: *\nAllow: /";
} else {
return "User-agent: Googlebot\nDisallow: /\n\nUser-agent: *\nDisallow: /";
}
}
}
@@ -1,231 +0,0 @@
package stirling.software.SPDF.controller.web;
import java.io.File;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.springframework.ui.Model;
import org.springframework.web.servlet.ModelAndView;
import io.swagger.v3.oas.annotations.Hidden;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import stirling.software.common.configuration.RuntimePathConfig;
import stirling.software.common.model.ApplicationProperties;
import stirling.software.common.util.CheckProgramInstall;
// @Controller // Disabled - Backend-only mode, no Thymeleaf UI
@RequiredArgsConstructor
@Slf4j
public class OtherWebController {
private final ApplicationProperties applicationProperties;
private final RuntimePathConfig runtimePathConfig;
@Deprecated
// @GetMapping("/compress-pdf")
@Hidden
public String compressPdfForm(Model model) {
model.addAttribute("currentPage", "compress-pdf");
return "misc/compress-pdf";
}
@Deprecated
// @GetMapping("/replace-and-invert-color-pdf")
@Hidden
public String replaceAndInvertColorPdfForm(Model model) {
model.addAttribute("currentPage", "replace-invert-color-pdf");
return "misc/replace-color";
}
@Deprecated
// @GetMapping("/extract-image-scans")
@Hidden
public ModelAndView extractImageScansForm() {
ModelAndView modelAndView = new ModelAndView("misc/extract-image-scans");
boolean isPython = CheckProgramInstall.isPythonAvailable();
modelAndView.addObject("isPython", isPython);
modelAndView.addObject("currentPage", "extract-image-scans");
return modelAndView;
}
@Deprecated
// @GetMapping("/show-javascript")
@Hidden
public String extractJavascriptForm(Model model) {
model.addAttribute("currentPage", "show-javascript");
return "misc/show-javascript";
}
@Deprecated
// @GetMapping("/stamp")
@Hidden
public String stampForm(Model model) {
model.addAttribute("currentPage", "stamp");
return "misc/stamp";
}
@Deprecated
// @GetMapping("/add-page-numbers")
@Hidden
public String addPageNumbersForm(Model model) {
model.addAttribute("currentPage", "add-page-numbers");
return "misc/add-page-numbers";
}
@Deprecated
// @GetMapping("/scanner-effect")
@Hidden
public String scannerEffectForm(Model model) {
model.addAttribute("currentPage", "scanner-effect");
return "misc/scanner-effect";
}
@Deprecated
// @GetMapping("/extract-images")
@Hidden
public String extractImagesForm(Model model) {
model.addAttribute("currentPage", "extract-images");
return "misc/extract-images";
}
@Deprecated
// @GetMapping("/flatten")
@Hidden
public String flattenForm(Model model) {
model.addAttribute("currentPage", "flatten");
return "misc/flatten";
}
@Deprecated
// @GetMapping("/change-metadata")
@Hidden
public String addWatermarkForm(Model model) {
model.addAttribute("currentPage", "change-metadata");
return "misc/change-metadata";
}
@Deprecated
// @GetMapping("/unlock-pdf-forms")
@Hidden
public String unlockPDFForms(Model model) {
model.addAttribute("currentPage", "unlock-pdf-forms");
return "misc/unlock-pdf-forms";
}
@Deprecated
// @GetMapping("/compare")
@Hidden
public String compareForm(Model model) {
model.addAttribute("currentPage", "compare");
return "misc/compare";
}
@Deprecated
// @GetMapping("/print-file")
@Hidden
public String printFileForm(Model model) {
model.addAttribute("currentPage", "print-file");
return "misc/print-file";
}
public List<String> getAvailableTesseractLanguages() {
String tessdataDir = runtimePathConfig.getTessDataPath();
File[] files = new File(tessdataDir).listFiles();
if (files == null) {
return Collections.emptyList();
}
return Arrays.stream(files)
.filter(file -> file.getName().endsWith(".traineddata"))
.map(file -> file.getName().replace(".traineddata", ""))
.filter(lang -> !"osd".equalsIgnoreCase(lang))
.sorted()
.toList();
}
@Deprecated
// @GetMapping("/ocr-pdf")
@Hidden
public ModelAndView ocrPdfPage() {
ModelAndView modelAndView = new ModelAndView("misc/ocr-pdf");
List<String> languages = getAvailableTesseractLanguages();
modelAndView.addObject("languages", languages);
modelAndView.addObject("currentPage", "ocr-pdf");
return modelAndView;
}
@Deprecated
// @GetMapping("/add-image")
@Hidden
public String overlayImage(Model model) {
model.addAttribute("currentPage", "add-image");
return "misc/add-image";
}
@Deprecated
// @GetMapping("/adjust-contrast")
@Hidden
public String contrast(Model model) {
model.addAttribute("currentPage", "adjust-contrast");
return "misc/adjust-contrast";
}
@Deprecated
// @GetMapping("/repair")
@Hidden
public String repairForm(Model model) {
model.addAttribute("currentPage", "repair");
return "misc/repair";
}
@Deprecated
// @GetMapping("/remove-blanks")
@Hidden
public String removeBlanksForm(Model model) {
model.addAttribute("currentPage", "remove-blanks");
return "misc/remove-blanks";
}
@Deprecated
// @GetMapping("/remove-annotations")
@Hidden
public String removeAnnotationsForm(Model model) {
model.addAttribute("currentPage", "remove-annotations");
return "misc/remove-annotations";
}
@Deprecated
// @GetMapping("/auto-crop")
@Hidden
public String autoCropForm(Model model) {
model.addAttribute("currentPage", "auto-crop");
return "misc/auto-crop";
}
@Deprecated
// @GetMapping("/auto-rename")
@Hidden
public String autoRenameForm(Model model) {
model.addAttribute("currentPage", "auto-rename");
return "misc/auto-rename";
}
@Deprecated
// @GetMapping("/add-attachments")
@Hidden
public String attachmentsForm(Model model) {
model.addAttribute("currentPage", "add-attachments");
return "misc/add-attachments";
}
@Deprecated
// @GetMapping("/extract-attachments")
@Hidden
public String extractAttachmentsForm(Model model) {
model.addAttribute("currentPage", "extract-attachments");
return "misc/extract-attachments";
}
}
@@ -1,98 +0,0 @@
package stirling.software.SPDF.controller.web;
import org.springframework.ui.Model;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.tags.Tag;
// @Controller // Disabled - Backend-only mode, no Thymeleaf UI
@Tag(name = "Security", description = "Security APIs")
public class SecurityWebController {
@Deprecated
// @GetMapping("/auto-redact")
@Hidden
public String autoRedactForm(Model model) {
model.addAttribute("currentPage", "auto-redact");
return "security/auto-redact";
}
@Deprecated
// @GetMapping("/redact")
public String redactForm(Model model) {
model.addAttribute("currentPage", "redact");
return "security/redact";
}
@Deprecated
// @GetMapping("/add-password")
@Hidden
public String addPasswordForm(Model model) {
model.addAttribute("currentPage", "add-password");
return "security/add-password";
}
@Deprecated
// @GetMapping("/change-permissions")
@Hidden
public String permissionsForm(Model model) {
model.addAttribute("currentPage", "change-permissions");
return "security/change-permissions";
}
@Deprecated
// @GetMapping("/remove-password")
@Hidden
public String removePasswordForm(Model model) {
model.addAttribute("currentPage", "remove-password");
return "security/remove-password";
}
@Deprecated
// @GetMapping("/add-watermark")
@Hidden
public String addWatermarkForm(Model model) {
model.addAttribute("currentPage", "add-watermark");
return "security/add-watermark";
}
@Deprecated
// @GetMapping("/cert-sign")
@Hidden
public String certSignForm(Model model) {
model.addAttribute("currentPage", "cert-sign");
return "security/cert-sign";
}
@Deprecated
// @GetMapping("/validate-signature")
@Hidden
public String certSignVerifyForm(Model model) {
model.addAttribute("currentPage", "validate-signature");
return "security/validate-signature";
}
@Deprecated
// @GetMapping("/remove-cert-sign")
@Hidden
public String certUnSignForm(Model model) {
model.addAttribute("currentPage", "remove-cert-sign");
return "security/remove-cert-sign";
}
@Deprecated
// @GetMapping("/sanitize-pdf")
@Hidden
public String sanitizeForm(Model model) {
model.addAttribute("currentPage", "sanitize-pdf");
return "security/sanitize-pdf";
}
@Deprecated
// @GetMapping("/get-info-on-pdf")
@Hidden
public String getInfo(Model model) {
model.addAttribute("currentPage", "get-info-on-pdf");
return "security/get-info-on-pdf";
}
}
@@ -1,14 +1,37 @@
package stirling.software.SPDF.model;
import java.util.ArrayList;
import java.util.List;
import org.springframework.core.io.Resource;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import stirling.software.common.util.TempFile;
@Data
public class PipelineResult {
@Slf4j
public class PipelineResult implements AutoCloseable {
private List<Resource> outputFiles;
private boolean hasErrors;
private boolean filtersApplied;
private List<TempFile> tempFiles = new ArrayList<>();
public void addTempFile(TempFile tempFile) {
tempFiles.add(tempFile);
}
@Override
public void close() {
for (TempFile file : tempFiles) {
file.close();
log.debug("Deleted temp file: {}", file.getAbsolutePath());
}
tempFiles.clear();
}
public void cleanup() {
close();
}
}
@@ -14,9 +14,6 @@ public class PdfToPdfARequest extends PDFFile {
@Schema(
description = "The output format type (PDF/A or PDF/X)",
requiredMode = Schema.RequiredMode.REQUIRED,
allowableValues = {
"pdfa", "pdfa-1", "pdfa-2", "pdfa-2b", "pdfa-3", "pdfa-3b", "pdfx", "pdfx-1",
"pdfx-3", "pdfx-4"
})
allowableValues = {"pdfa", "pdfa-1", "pdfa-2", "pdfa-2b", "pdfa-3", "pdfa-3b", "pdfx"})
private String outputFormat;
}
@@ -0,0 +1,29 @@
package stirling.software.SPDF.model.api.converters;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode
public class SvgToPdfRequest {
@Schema(
description =
"The SVG file(s) to be converted to PDF. "
+ "SVGs are scalable and have inherent dimensions - the conversion uses these dimensions "
+ "to determine the PDF page size. If dimensions are not specified in the SVG, A4 size is used.",
requiredMode = Schema.RequiredMode.REQUIRED)
private MultipartFile[] fileInput;
@Schema(
description =
"Whether to combine all SVG files into a single PDF (each SVG as a separate page) "
+ "or create separate PDF files for each SVG.",
requiredMode = Schema.RequiredMode.REQUIRED,
defaultValue = "false")
private Boolean combineIntoSinglePdf;
}
@@ -39,6 +39,13 @@ public class AddPageNumbersRequest extends PDFWithPageNums {
requiredMode = RequiredMode.NOT_REQUIRED)
private String fontColor;
@Schema(
description = "Zero-padding width for page numbers (Bates Stamping). Set to 0 to disable padding",
minimum = "0",
defaultValue = "0",
requiredMode = RequiredMode.NOT_REQUIRED)
private int zeroPad = 0;
@Schema(
description =
"Position: 1-9 representing positions on the page (1=top-left, 2=top-center,"
@@ -32,8 +32,8 @@ public class AddStampRequest extends PDFWithPageNums {
private String alphabet = "roman";
@Schema(
description = "The font size of the stamp text and image",
defaultValue = "30",
description = "The font size of the stamp text and image in points.",
defaultValue = "40",
requiredMode = Schema.RequiredMode.REQUIRED)
private float fontSize;
@@ -14,7 +14,10 @@ import stirling.software.common.model.api.PDFFile;
public class OverlayImageRequest extends PDFFile {
@Schema(
description = "The image file to be overlaid onto the PDF.",
description =
"The image file to be overlaid onto the PDF. "
+ "Supports raster formats (PNG, JPEG, etc.) and vector format (SVG). "
+ "SVG files are rendered as vector graphics for crisp output at any resolution.",
requiredMode = Schema.RequiredMode.REQUIRED,
format = "binary")
private MultipartFile imageFile;
@@ -2,6 +2,7 @@ package stirling.software.SPDF.service;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URL;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
@@ -510,7 +511,7 @@ public class CertificateValidationService {
private byte[] downloadTrustList(String urlStr) {
HttpURLConnection conn = null;
try {
URL url = new URL(urlStr);
URL url = URI.create(urlStr).toURL();
conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(10_000);
@@ -700,7 +701,7 @@ public class CertificateValidationService {
private byte[] downloadXml(String urlStr) {
HttpURLConnection conn = null;
try {
URL url = new URL(urlStr);
URL url = URI.create(urlStr).toURL();
conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(10_000);
@@ -3424,7 +3424,7 @@ public class PdfJsonConversionService {
}
break;
case "Tj":
if (i == 0 || !(tokens.get(i - 1) instanceof COSString cosString)) {
if (i == 0 || !(tokens.get(i - 1) instanceof COSString)) {
log.debug(
"Encountered Tj without preceding string operand; aborting rewrite");
return false;
@@ -3435,7 +3435,8 @@ public class PdfJsonConversionService {
i,
cursor.remaining());
if (!rewriteShowText(
cosString,
tokens,
i - 1,
currentFont,
currentFontModel,
currentFontName,
@@ -3496,7 +3497,8 @@ public class PdfJsonConversionService {
}
private boolean rewriteShowText(
COSString cosString,
List<Object> tokens,
int tokenIndex,
PDFont font,
PdfJsonFont fontModel,
String expectedFontName,
@@ -3509,6 +3511,7 @@ public class PdfJsonConversionService {
expectedFontName);
return false;
}
COSString cosString = (COSString) tokens.get(tokenIndex);
int glyphCount = countGlyphs(cosString, font);
log.trace(
"rewriteShowText consuming {} glyphs at cursor index {} for font {}",
@@ -3525,7 +3528,7 @@ public class PdfJsonConversionService {
return false;
}
if (removeOnly) {
cosString.setValue(new byte[0]);
tokens.set(tokenIndex, new COSString(new byte[0]));
return true;
}
MergedText replacement = mergeText(consumed);
@@ -3540,7 +3543,7 @@ public class PdfJsonConversionService {
replacement.text());
return false;
}
cosString.setValue(encoded);
tokens.set(tokenIndex, new COSString(encoded));
return true;
} catch (IOException | IllegalArgumentException | UnsupportedOperationException ex) {
log.debug(
@@ -82,7 +82,7 @@ public class TelegramPipelineBot extends TelegramLongPollingBot {
@PostConstruct
public void register() {
if (StringUtils.isAnyBlank(getBotUsername(), getBotToken())) {
if (StringUtils.isAnyBlank(getBotUsername(), this.telegramProperties.getBotToken())) {
log.warn("Telegram bot disabled because botToken or botUsername is not configured");
return;
}
@@ -392,7 +392,7 @@ public class TelegramPipelineBot extends TelegramLongPollingBot {
new URI(
"https",
"api.telegram.org",
"/file/bot" + getBotToken() + "/" + filePath,
"/file/bot" + this.telegramProperties.getBotToken() + "/" + filePath,
null);
return uri.toURL();
} catch (URISyntaxException e) {
@@ -516,9 +516,4 @@ public class TelegramPipelineBot extends TelegramLongPollingBot {
public String getBotUsername() {
return telegramProperties.getBotUsername();
}
@Override
public String getBotToken() {
return telegramProperties.getBotToken();
}
}
@@ -0,0 +1,97 @@
package stirling.software.SPDF.utils;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import org.apache.batik.anim.dom.SAXSVGDocumentFactory;
import org.apache.batik.bridge.BridgeContext;
import org.apache.batik.bridge.DocumentLoader;
import org.apache.batik.bridge.GVTBuilder;
import org.apache.batik.bridge.UserAgent;
import org.apache.batik.bridge.UserAgentAdapter;
import org.apache.batik.gvt.GraphicsNode;
import org.apache.batik.util.XMLResourceDescriptor;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
import org.apache.pdfbox.util.Matrix;
import org.w3c.dom.svg.SVGDocument;
import lombok.experimental.UtilityClass;
import lombok.extern.slf4j.Slf4j;
import de.rototor.pdfbox.graphics2d.PdfBoxGraphics2D;
@UtilityClass
@Slf4j
public class SvgOverlayUtil {
public void overlaySvgOnPage(
PDDocument document, PDPage page, byte[] svgBytes, float x, float y)
throws IOException {
try {
String parser = XMLResourceDescriptor.getXMLParserClassName();
SAXSVGDocumentFactory factory = new SAXSVGDocumentFactory(parser);
SVGDocument svgDoc;
try (ByteArrayInputStream inputStream = new ByteArrayInputStream(svgBytes)) {
svgDoc = factory.createSVGDocument("file:///overlay.svg", inputStream);
}
UserAgent userAgent = new UserAgentAdapter();
DocumentLoader loader = new DocumentLoader(userAgent);
BridgeContext ctx = new BridgeContext(userAgent, loader);
ctx.setDynamicState(BridgeContext.DYNAMIC);
GVTBuilder builder = new GVTBuilder();
GraphicsNode rootNode = builder.build(ctx, svgDoc);
float svgWidth = (float) ctx.getDocumentSize().getWidth();
float svgHeight = (float) ctx.getDocumentSize().getHeight();
PdfBoxGraphics2D pdfGraphics = new PdfBoxGraphics2D(document, svgWidth, svgHeight);
try {
rootNode.paint(pdfGraphics);
} finally {
pdfGraphics.dispose();
}
PDFormXObject xform = pdfGraphics.getXFormObject();
try (PDPageContentStream newContentStream =
new PDPageContentStream(
document, page, PDPageContentStream.AppendMode.APPEND, true, true)) {
newContentStream.saveGraphicsState();
newContentStream.transform(new Matrix(1, 0, 0, 1, x, y));
newContentStream.drawForm(xform);
newContentStream.restoreGraphicsState();
}
log.info("SVG successfully overlaid as vector graphic at ({}, {})", x, y);
} catch (Exception e) {
log.error("Failed to overlay SVG as vector graphic", e);
throw new IOException("SVG overlay failed: " + e.getMessage(), e);
}
}
public boolean isSvgImage(byte[] bytes) {
if (bytes == null || bytes.length < 5) {
return false;
}
// Check for SVG markers: <?xml or <svg
String start =
new String(
bytes,
0,
Math.min(200, bytes.length),
java.nio.charset.StandardCharsets.UTF_8)
.toLowerCase();
return start.contains("<svg") || (start.contains("<?xml") && start.contains("svg"));
}
}
@@ -0,0 +1,234 @@
package stirling.software.SPDF.utils;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.apache.batik.anim.dom.SAXSVGDocumentFactory;
import org.apache.batik.bridge.BridgeContext;
import org.apache.batik.bridge.DocumentLoader;
import org.apache.batik.bridge.GVTBuilder;
import org.apache.batik.bridge.UserAgent;
import org.apache.batik.bridge.UserAgentAdapter;
import org.apache.batik.gvt.GraphicsNode;
import org.apache.batik.util.XMLResourceDescriptor;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
import org.w3c.dom.svg.SVGDocument;
import lombok.experimental.UtilityClass;
import lombok.extern.slf4j.Slf4j;
import de.rototor.pdfbox.graphics2d.PdfBoxGraphics2D;
@UtilityClass
@Slf4j
public class SvgToPdf {
/** Default page width in points (A4) */
private static final float DEFAULT_PAGE_WIDTH = 595f;
/** Default page height in points (A4) */
private static final float DEFAULT_PAGE_HEIGHT = 842f;
/** Timeout for SVG rendering in seconds (prevents DoS via complex SVGs) */
private static final int RENDERING_TIMEOUT_SECONDS = 30;
public byte[] convert(byte[] svgBytes) throws IOException {
if (svgBytes == null || svgBytes.length == 0) {
throw new IOException("SVG input is empty or null");
}
log.debug("Starting SVG to PDF conversion, input size: {} bytes", svgBytes.length);
try {
// 1. Load SVG using Batik
String parser = XMLResourceDescriptor.getXMLParserClassName();
SAXSVGDocumentFactory factory = new SAXSVGDocumentFactory(parser);
SVGDocument svgDoc;
try (ByteArrayInputStream inputStream = new ByteArrayInputStream(svgBytes)) {
svgDoc = factory.createSVGDocument("file:///input.svg", inputStream);
}
// 2. Build the GVT (Graphics Vector Tree) with timeout protection
UserAgent userAgent = new UserAgentAdapter();
DocumentLoader loader = new DocumentLoader(userAgent);
BridgeContext ctx = new BridgeContext(userAgent, loader);
ctx.setDynamicState(BridgeContext.DYNAMIC);
GraphicsNode rootNode = buildGvtWithTimeout(ctx, svgDoc);
// 3. Get SVG dimensions
float width = (float) ctx.getDocumentSize().getWidth();
float height = (float) ctx.getDocumentSize().getHeight();
if (width <= 0) {
width = DEFAULT_PAGE_WIDTH;
log.warn("SVG width not specified, using default: {}", width);
}
if (height <= 0) {
height = DEFAULT_PAGE_HEIGHT;
log.warn("SVG height not specified, using default: {}", height);
}
log.debug("SVG dimensions: {}x{} points", width, height);
// 4. Create PDF document and render
return renderToPdf(rootNode, width, height);
} catch (Exception e) {
log.error("Failed to convert SVG to PDF", e);
throw new IOException("SVG to PDF conversion failed: " + e.getMessage(), e);
}
}
private GraphicsNode buildGvtWithTimeout(BridgeContext ctx, SVGDocument svgDoc)
throws IOException {
GVTBuilder builder = new GVTBuilder();
ExecutorService executor = Executors.newSingleThreadExecutor();
Callable<GraphicsNode> buildTask = () -> builder.build(ctx, svgDoc);
Future<GraphicsNode> future = executor.submit(buildTask);
try {
return future.get(RENDERING_TIMEOUT_SECONDS, TimeUnit.SECONDS);
} catch (TimeoutException e) {
future.cancel(true);
throw new IOException(
"SVG rendering timed out after "
+ RENDERING_TIMEOUT_SECONDS
+ " seconds. The SVG may be too complex.");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IOException("SVG rendering was interrupted", e);
} catch (ExecutionException e) {
Throwable cause = e.getCause();
throw new IOException(
"SVG rendering failed: "
+ (cause != null ? cause.getMessage() : e.getMessage()),
cause);
} finally {
executor.shutdownNow();
}
}
private byte[] renderToPdf(GraphicsNode rootNode, float width, float height)
throws IOException {
try (PDDocument document = new PDDocument();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
PDPage page = new PDPage(new PDRectangle(width, height));
document.addPage(page);
// Create and use PdfBoxGraphics2D with proper resource management
PdfBoxGraphics2D pdfGraphics = new PdfBoxGraphics2D(document, width, height);
try {
rootNode.paint(pdfGraphics);
} finally {
pdfGraphics.dispose();
}
PDFormXObject xform = pdfGraphics.getXFormObject();
try (PDPageContentStream contentStream = new PDPageContentStream(document, page)) {
contentStream.drawForm(xform);
}
document.save(outputStream);
byte[] result = outputStream.toByteArray();
log.debug("SVG to PDF conversion complete, output size: {} bytes", result.length);
return result;
}
}
public byte[] combineIntoPdf(List<byte[]> svgBytesList) throws IOException {
if (svgBytesList == null || svgBytesList.isEmpty()) {
throw new IOException("SVG list is empty or null");
}
log.debug("Combining {} SVG files into single PDF", svgBytesList.size());
try (PDDocument document = new PDDocument();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
for (int i = 0; i < svgBytesList.size(); i++) {
byte[] svgBytes = svgBytesList.get(i);
if (svgBytes == null || svgBytes.length == 0) {
log.warn("Skipping empty SVG at index {}", i);
continue;
}
try {
addSvgAsPage(document, svgBytes);
log.debug("Added SVG {} of {} to combined PDF", i + 1, svgBytesList.size());
} catch (Exception e) {
log.error("Failed to add SVG {} to combined PDF: {}", i, e.getMessage());
// Continue with other SVGs
}
}
if (document.getNumberOfPages() == 0) {
throw new IOException("No SVG files were successfully added to the PDF");
}
document.save(outputStream);
byte[] result = outputStream.toByteArray();
log.debug(
"Combined SVG to PDF conversion complete, output size: {} bytes",
result.length);
return result;
}
}
private void addSvgAsPage(PDDocument document, byte[] svgBytes) throws IOException {
String parser = XMLResourceDescriptor.getXMLParserClassName();
SAXSVGDocumentFactory factory = new SAXSVGDocumentFactory(parser);
SVGDocument svgDoc;
try (ByteArrayInputStream inputStream = new ByteArrayInputStream(svgBytes)) {
svgDoc = factory.createSVGDocument("file:///input.svg", inputStream);
}
UserAgent userAgent = new UserAgentAdapter();
DocumentLoader loader = new DocumentLoader(userAgent);
BridgeContext ctx = new BridgeContext(userAgent, loader);
ctx.setDynamicState(BridgeContext.DYNAMIC);
GraphicsNode rootNode = buildGvtWithTimeout(ctx, svgDoc);
float svgWidth = (float) ctx.getDocumentSize().getWidth();
float svgHeight = (float) ctx.getDocumentSize().getHeight();
if (svgWidth <= 0) svgWidth = DEFAULT_PAGE_WIDTH;
if (svgHeight <= 0) svgHeight = DEFAULT_PAGE_HEIGHT;
// Use SVG dimensions directly for the PDF page
PDPage page = new PDPage(new PDRectangle(svgWidth, svgHeight));
document.addPage(page);
PdfBoxGraphics2D pdfGraphics = new PdfBoxGraphics2D(document, svgWidth, svgHeight);
try {
rootNode.paint(pdfGraphics);
} finally {
pdfGraphics.dispose();
}
PDFormXObject xform = pdfGraphics.getXFormObject();
try (PDPageContentStream contentStream = new PDPageContentStream(document, page)) {
contentStream.drawForm(xform);
}
}
}
@@ -33,7 +33,6 @@ server.servlet.context-path=${SYSTEM_ROOTURIPATH:/}
spring.devtools.restart.enabled=true
spring.devtools.livereload.enabled=true
spring.devtools.restart.exclude=stirling.software.proprietary.security/**
# spring.thymeleaf.encoding=UTF-8 # Disabled - React frontend replaces Thymeleaf
spring.web.resources.mime-mappings.webmanifest=application/manifest+json
spring.mvc.async.request-timeout=${SYSTEM_CONNECTIONTIMEOUTMILLISECONDS:1200000}
@@ -81,6 +81,7 @@ security:
revocation:
mode: none # Revocation checking mode: 'none' (disabled), 'ocsp' (OCSP only), 'crl' (CRL only), 'ocsp+crl' (OCSP with CRL fallback)
hardFail: false # Fail validation if revocation status cannot be determined (true=strict, false=soft-fail)
xFrameOptions: DENY # X-Frame-Options header value. Options: 'DENY' (default, prevents all framing), 'SAMEORIGIN' (allows framing from same domain), 'DISABLED' (no X-Frame-Options header sent). Note: automatically set to DISABLED when login is disabled
premium:
key: 00000000-0000-0000-0000-000000000000
@@ -202,7 +203,9 @@ system:
name: postgres # set the name of your database. Should match the name of the database you create
customPaths:
pipeline:
pipelineDir: "" # Defaults to /pipeline
watchedFoldersDir: "" # Defaults to /pipeline/watchedFolders
watchedFoldersDirs: [] # List of watched folder directories. Defaults to watchedFoldersDir or /pipeline/watchedFolders.
finishedFoldersDir: "" # Defaults to /pipeline/finishedFolders
operations:
weasyprint: "" # Defaults to /opt/venv/bin/weasyprint
@@ -242,8 +245,20 @@ AutomaticallyGenerated:
appVersion: 0.35.0
processExecutor:
autoUnoServer: true # true: use local pool based on libreOfficeSessionLimit; false: use unoServerEndpoints
unoServerEndpoints: [] # Used when autoUnoServer is false
# Example manual endpoints (uncomment to use):
# unoServerEndpoints:
# - host: "127.0.0.1"
# port: 2003
# hostLocation: "auto" # auto|local|remote (use "remote" for port-forwarded servers)
# protocol: "http" # http|https
# - host: "remote-server.local"
# port: 8080
# hostLocation: "remote"
# protocol: "https"
sessionLimit: # Process executor instances limits
libreOfficeSessionLimit: 1
libreOfficeSessionLimit: 1 # Each additional uno server adds ~50MB idle RAM
pdfToHtmlSessionLimit: 1
qpdfSessionLimit: 4
tesseractSessionLimit: 1
File diff suppressed because it is too large Load Diff
@@ -1,21 +0,0 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title='<3')}"></th:block>
</head>
<body>
<div id="page-container">
<div id="content-wrap">
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<br><br>
<div class="container">
<div class="row justify-content-center">
<div class="col-md-6 bg-card"></div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>
@@ -1,479 +0,0 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{account.title})}"></th:block>
<link rel="stylesheet" th:href="@{/css/modern-tables.css}">
</head>
<body>
<th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container">
<div id="content-wrap">
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<div class="data-container">
<div class="data-panel">
<div class="data-header">
<h1 class="data-title">
<span class="data-icon">
<span class="material-symbols-rounded">settings_account_box</span>
</span>
<span th:text="#{account.accountSettings}">User Settings</span>
</h1>
</div>
<div class="data-body">
<div th:if="${messageType}" class="alert alert-danger data-mb-3">
<span th:text="#{${messageType}}">Default message if not found</span>
</div>
<div th:if="${error}" class="alert alert-danger data-mb-3" role="alert">
<span th:text="${error}">Error Message</span>
</div>
<!-- Admin Settings Banner (for admins only) -->
<div th:if="${role == 'ROLE_ADMIN'}" class="data-panel data-mb-3" style="background-color: var(--md-sys-color-secondary-container);">
<div class="data-body" style="display: flex; align-items: center; justify-content: space-between; padding: 1rem 1.5rem; background-color: var(--md-sys-color-secondary-container);">
<div style="display: flex; align-items: center; gap: 1rem;">
<span class="material-symbols-rounded" style="font-size: 2rem; color: var(--md-sys-color-secondary);">
admin_panel_settings
</span>
<div>
<h4 style="margin: 0; color: var(--md-sys-color-secondary);" th:text="#{account.adminTitle}">Administrator Tools</h4>
<p style="margin: 0.25rem 0 0 0; color: var(--md-sys-color-secondary);" th:text="#{account.adminNotif}">You have admin privileges. Access system settings and user management.</p>
</div>
</div>
<a class="data-btn" th:href="@{'/adminSettings'}" role="button" target="_blank"
style="background-color: var(--md-sys-color-secondary); color: var(--md-sys-color-on-secondary); display: inline-flex; align-items: center; gap: 0.5rem; padding: 0.625rem 1.25rem; border-radius: 0.5rem; font-weight: 500; border: none; cursor: pointer; text-decoration: none;">
<span class="material-symbols-rounded">admin_panel_settings</span>
<span th:text="#{account.adminSettings}">Admin Settings</span>
</a>
</div>
</div>
<!-- Account Management Buttons -->
<th:block th:if="not ${oAuth2Login} or not ${saml2Login}">
<div class="data-section-title">Account Management</div>
<div class="data-actions data-actions-start data-mb-3">
<button class="data-btn data-btn-primary" data-bs-toggle="modal" data-bs-target="#changeUsernameModal">
<span class="material-symbols-rounded">edit</span>
<span th:text="#{account.changeUsername}">Change Username</span>
</button>
<button class="data-btn data-btn-primary" data-bs-toggle="modal" data-bs-target="#changePasswordModal">
<span class="material-symbols-rounded">key</span>
<span th:text="#{account.changePassword}">Change Password</span>
</button>
</div>
</th:block>
<!-- API Key Section -->
<div class="data-section-title" th:text="#{account.yourApiKey}">API Key</div>
<div class="data-panel data-mb-3">
<div class="data-header">
<h5 class="data-title">
<span class="data-icon">
<span class="material-symbols-rounded">key</span>
</span>
<span th:text="#{account.yourApiKey}">API Key</span>
</h5>
</div>
<div class="data-body">
<div style="display: flex; gap: 0.5rem;">
<input class="data-form-control" id="apiKey" th:placeholder="#{account.yourApiKey}" readonly style="flex: 1;">
<button class="data-btn data-btn-secondary" id="copyBtn" type="button" onclick="copyToClipboard()" title="Copy to clipboard">
<span class="material-symbols-rounded">content_copy</span>
</button>
<button class="data-btn data-btn-secondary" id="showBtn" type="button" onclick="showApiKey()" title="Show/hide API key">
<span class="material-symbols-rounded" id="eyeIcon">visibility</span>
</button>
<button class="data-btn data-btn-secondary" id="refreshBtn" type="button" onclick="refreshApiKey()" title="Refresh API key">
<span class="material-symbols-rounded">refresh</span>
</button>
</div>
</div>
</div>
<!-- Settings Sync Section -->
<div class="data-section-title" th:text="#{account.syncTitle}">Sync browser settings with Account</div>
<div class="data-panel data-mb-3">
<div class="data-header">
<h5 class="data-title">
<span class="data-icon">
<span class="material-symbols-rounded">sync</span>
</span>
<span th:text="#{account.settingsCompare}">Settings Comparison</span>
</h5>
</div>
<div class="data-body">
<div class="table-responsive">
<table id="settingsTable" class="data-table">
<thead>
<tr>
<th scope="col" th:text="#{account.property}">Property</th>
<th scope="col" th:text="#{account.accountSettings}">Account Setting</th>
<th scope="col" th:text="#{account.webBrowserSettings}">Web Browser Setting</th>
</tr>
</thead>
<tbody>
<!-- This will be dynamically populated by JavaScript -->
</tbody>
</table>
</div>
<div class="data-actions data-mt-3">
<button id="syncToBrowser" class="data-btn data-btn-primary">
<span class="material-symbols-rounded">cloud_download</span>
<span th:text="#{account.syncToBrowser}">Sync Account -> Browser</span>
</button>
<button id="syncToAccount" class="data-btn data-btn-secondary">
<span class="material-symbols-rounded">cloud_upload</span>
<span th:text="#{account.syncToAccount}">Sync Account <- Browser</span>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Change Username Modal -->
<div class="modal fade" id="changeUsernameModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<form id="formsavechangeusername" th:action="@{'/api/v1/user/change-username'}" method="post" class="modal-content data-modal">
<div class="data-modal-header">
<h5 class="data-modal-title">
<span class="data-icon">
<span class="material-symbols-rounded">edit</span>
</span>
<span th:text="#{account.changeUsername}">Change Username</span>
</h5>
<button type="button" class="data-btn-close" data-bs-dismiss="modal" aria-label="Close">
<span class="material-symbols-rounded">close</span>
</button>
</div>
<div class="data-modal-body">
<div class="data-form-group">
<label for="newUsername" class="data-form-label" th:text="#{account.newUsername}">New Username</label>
<input type="text" class="data-form-control" name="newUsername" id="newUsername" th:placeholder="#{account.newUsername}">
<span id="usernameError" style="display: none; color: var(--md-sys-color-error);" th:text="#{invalidUsernameMessage}">Invalid username!</span>
</div>
<div class="data-form-group">
<label for="currentPasswordChangeUsername" class="data-form-label" th:text="#{password}">Password</label>
<input type="password" class="data-form-control" name="currentPasswordChangeUsername" id="currentPasswordChangeUsername" th:placeholder="#{password}">
</div>
<div class="data-form-actions">
<button type="button" class="data-btn data-btn-secondary" data-bs-dismiss="modal">
<span class="material-symbols-rounded">close</span>
<span th:text="#{cancel}">Cancel</span>
</button>
<button type="submit" class="data-btn data-btn-primary">
<span class="material-symbols-rounded">check</span>
<span th:text="#{account.changeUsername}">Change Username</span>
</button>
</div>
</div>
</form>
</div>
</div>
<!-- Change Password Modal -->
<div class="modal fade" id="changePasswordModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<form id="formsavechangepassword" th:action="@{'/api/v1/user/change-password'}" method="post" class="modal-content data-modal">
<div class="data-modal-header">
<h5 class="data-modal-title">
<span class="data-icon">
<span class="material-symbols-rounded">key</span>
</span>
<span th:text="#{account.changePassword}">Change Password</span>
</h5>
<button type="button" class="data-btn-close" data-bs-dismiss="modal" aria-label="Close">
<span class="material-symbols-rounded">close</span>
</button>
</div>
<div class="data-modal-body">
<div class="data-form-group">
<label for="currentPassword" class="data-form-label" th:text="#{account.oldPassword}">Old Password</label>
<input type="password" class="data-form-control" name="currentPassword" id="currentPassword" th:placeholder="#{account.oldPassword}">
</div>
<div class="data-form-group">
<label for="newPassword" class="data-form-label" th:text="#{account.newPassword}">New Password</label>
<input type="password" class="data-form-control" name="newPassword" id="newPassword" th:placeholder="#{account.newPassword}">
</div>
<div class="data-form-group">
<label for="confirmNewPassword" class="data-form-label" th:text="#{account.confirmNewPassword}">Confirm New Password</label>
<input type="password" class="data-form-control" name="confirmNewPassword" id="confirmNewPassword" th:placeholder="#{account.confirmNewPassword}">
<span id="confirmPasswordError" style="display: none; color: var(--md-sys-color-error);" th:text="#{confirmPasswordErrorMessage}">New Password and Confirm New Password must match.</span>
</div>
<div class="data-form-actions">
<button type="button" class="data-btn data-btn-secondary" data-bs-dismiss="modal">
<span class="material-symbols-rounded">close</span>
<span th:text="#{cancel}">Cancel</span>
</button>
<button type="submit" class="data-btn data-btn-primary">
<span class="material-symbols-rounded">check</span>
<span th:text="#{account.changePassword}">Change Password</span>
</button>
</div>
</div>
</form>
</div>
</div>
<!-- JavaScript for validation -->
<script th:inline="javascript">
jQuery.validator.addMethod("usernamePattern", function(value, element) {
// Regular expression for user name: Min. 3 characters, max. 50 characters
const regexUsername = /^[a-zA-Z0-9](?!.*[-@._+]{2,})([a-zA-Z0-9@._+-]{1,48})[a-zA-Z0-9]$/;
// Regular expression for email addresses: Max. 320 characters, with RFC-like validation
const regexEmail = /^(?=.{1,320}$)(?=.{1,64}@)[A-Za-z0-9](?:[A-Za-z0-9_.+-]*[A-Za-z0-9])?@[^-][A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*(?:\.[A-Za-z]{2,})$/;
// Check if the field is optional or meets the requirements
return this.optional(element) || regexUsername.test(value) || regexEmail.test(value);
}, /*[[#{invalidUsernameMessage}]]*/ "Invalid username format");
$(document).ready(function() {
$.validator.addMethod("passwordMatch", function(value, element) {
return $('#newPassword').val() === $('#confirmNewPassword').val();
}, /*[[#{confirmPasswordErrorMessage}]]*/ "New Password and Confirm New Password must match.");
$('#formsavechangepassword').validate({
rules: {
currentPassword: {
required: true
},
newPassword: {
required: true
},
confirmNewPassword: {
required: true,
passwordMatch: true
}
},
errorPlacement: function(error, element) {
if (element.attr("name") === "newPassword" || element.attr("name") === "confirmNewPassword") {
$("#confirmPasswordError").text(error.text()).show();
} else {
error.insertAfter(element);
}
},
success: function(label, element) {
if ($(element).attr("name") === "newPassword" || $(element).attr("name") === "confirmNewPassword") {
$("#confirmPasswordError").hide();
}
}
});
$('#formsavechangeusername').validate({
rules: {
newUsername: {
required: true,
usernamePattern: true
},
currentPasswordChangeUsername: {
required: true
}
},
messages: {
newUsername: {
usernamePattern: /*[[#{invalidUsernameMessage}]]*/ "Invalid username format"
},
},
errorPlacement: function(error, element) {
if (element.attr("name") === "newUsername") {
$("#usernameError").text(error.text()).show();
} else {
error.insertAfter(element);
}
},
success: function(label, element) {
if ($(element).attr("name") === "newUsername") {
$("#usernameError").hide();
}
}
});
});
</script>
<!-- JavaScript for API Key -->
<script th:inline="javascript">
function copyToClipboard() {
const apiKeyElement = document.getElementById("apiKey");
apiKeyElement.select();
document.execCommand("copy");
}
function showApiKey() {
const apiKeyElement = document.getElementById("apiKey");
const copyBtn = document.getElementById("copyBtn");
const eyeIcon = document.getElementById("eyeIcon");
if (apiKeyElement.type === "password") {
apiKeyElement.type = "text";
eyeIcon.textContent = "visibility_off";
copyBtn.disabled = false; // Enable copy button when API key is visible
} else {
apiKeyElement.type = "password";
eyeIcon.textContent = "visibility";
copyBtn.disabled = true; // Disable copy button when API key is hidden
}
}
document.addEventListener("DOMContentLoaded", async function() {
try {
/*<![CDATA[*/
const urlGetApiKey = /*[[@{/api/v1/user/get-api-key}]]*/ "/api/v1/user/get-api-key";
/*]]>*/
let response = await window.fetchWithCsrf(urlGetApiKey, { method: 'POST' });
if (response.status === 200) {
let apiKey = await response.text();
manageUIState(apiKey);
} else {
manageUIState(null);
}
} catch (error) {
console.error('There was an error:', error);
}
finally {
showApiKey();
}
});
async function refreshApiKey() {
try {
/*<![CDATA[*/
const urlUpdateApiKey = /*[[@{/api/v1/user/update-api-key}]]*/ "/api/v1/user/update-api-key";
/*]]>*/
let response = await window.fetchWithCsrf(urlUpdateApiKey, { method: 'POST' });
if (response.status === 200) {
let apiKey = await response.text();
manageUIState(apiKey);
document.getElementById("apiKey").type = 'text';
document.getElementById("copyBtn").disabled = false;
} else {
alert('Error refreshing API key.');
}
} catch (error) {
console.error('There was an error:', error);
}
}
function manageUIState(apiKey) {
const apiKeyElement = document.getElementById("apiKey");
const showBtn = document.getElementById("showBtn");
const copyBtn = document.getElementById("copyBtn");
if (apiKey && apiKey.trim().length > 0) {
apiKeyElement.value = apiKey;
showBtn.disabled = false;
copyBtn.disabled = false;
} else {
apiKeyElement.value = "";
showBtn.disabled = true;
copyBtn.disabled = true;
}
}
</script>
<!-- JavaScript for Settings Sync -->
<script th:inline="javascript">
document.addEventListener("DOMContentLoaded", async function() {
const settingsTableBody = document.querySelector("#settingsTable tbody");
// Helper function to check if a key should be ignored
function shouldIgnoreKey(key) {
return key === 'debug' ||
key === '0' ||
key === '1' ||
key.includes('pdfjs') ||
key.includes('clientSubmissionOrder') ||
key.includes('lastSubmitTime') ||
key.includes('lastClientId') ||
key.includes('stirling_jwt') ||
key.includes('JSESSIONID') ||
key.includes('XSRF-TOKEN') ||
key.includes('remember-me') ||
key.includes('auth') ||
key.includes('token') ||
key.includes('session') ||
key.includes('posthog') || key.includes('ssoRedirectAttempts') || key.includes('lastRedirectAttempt') || key.includes('surveyVersion') ||
key.includes('pageViews');
}
/*<![CDATA[*/
var accountSettingsString = /*[[${settings}]]*/ {};
/*]]>*/
var accountSettings = JSON.parse(accountSettingsString);
let allKeys = new Set([...Object.keys(accountSettings), ...Object.keys(localStorage)]);
allKeys.forEach(key => {
if(shouldIgnoreKey(key)) return; // Using our helper function
const accountValue = accountSettings[key] || '-';
const browserValue = localStorage.getItem(key) || '-';
const row = settingsTableBody.insertRow();
const propertyCell = row.insertCell(0);
const accountCell = row.insertCell(1);
const browserCell = row.insertCell(2);
propertyCell.textContent = key;
accountCell.textContent = accountValue;
browserCell.textContent = browserValue;
});
document.getElementById('syncToBrowser').addEventListener('click', function() {
// First, clear the local storage
localStorage.clear();
// Then, set the account settings to local storage
for (let key in accountSettings) {
if(!shouldIgnoreKey(key)) { // Using our helper function
localStorage.setItem(key, accountSettings[key]);
}
}
location.reload(); // Refresh the page after sync
});
document.getElementById('syncToAccount').addEventListener('click', async function() {
/*<![CDATA[*/
const urlUpdateUserSettings = /*[[@{/api/v1/user/updateUserSettings}]]*/ "/api/v1/user/updateUserSettings";
/*]]>*/
let settings = {};
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if(!shouldIgnoreKey(key)) { // Using our helper function
settings[key] = localStorage.getItem(key);
}
}
try {
const response = await window.fetchWithCsrf(urlUpdateUserSettings, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(settings)
});
if (response.ok) {
location.reload();
} else {
alert('Error syncing settings to account');
}
} catch (error) {
console.error('Error:', error);
alert('Error syncing settings to account');
}
});
});
</script>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>
@@ -1,458 +0,0 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{adminUserSettings.title}, header=#{adminUserSettings.header})}"></th:block>
<link rel="stylesheet" th:href="@{/css/modern-tables.css}">
<style>
.active-user {
color: var(--md-sys-color-tertiary);
font-weight: 600;
}
</style>
</head>
<body>
<th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container">
<div id="content-wrap">
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<div class="data-container">
<div class="data-panel">
<div class="data-header">
<h1 class="data-title">
<span class="data-icon">
<span class="material-symbols-rounded">manage_accounts</span>
</span>
<span th:text="#{adminUserSettings.header}">Admin User Control Settings</span>
</h1>
</div>
<div class="data-body">
<!-- User Stats Banner -->
<div class="data-panel data-mb-3" style="background-color: var(--md-sys-color-primary-container);">
<div class="data-body" style="padding: 1.25rem;">
<div style="display: flex; flex-wrap: wrap; justify-content: space-around; align-items: center; gap: 1.5rem;">
<div style="display: flex; align-items: center; gap: 0.75rem;">
<span class="material-symbols-rounded" style="font-size: 2.25rem; color: var(--md-sys-color-primary);">
group
</span>
<div>
<div style="color: var(--md-sys-color-primary); font-size: 0.875rem; font-weight: 500;" th:text="#{adminUserSettings.totalUsers}">Total Users</div>
<div style="color: var(--md-sys-color-primary); font-size: 1.5rem; font-weight: 700;">
<span th:text="${totalUsers}"></span>
<span th:if="${@runningProOrHigher}" th:text="'/' + ${maxPaidUsers}" style="font-size: 1rem;"></span>
</div>
</div>
</div>
<div style="display: flex; align-items: center; gap: 0.75rem;">
<span class="material-symbols-rounded" style="font-size: 2.25rem; color: var(--md-sys-color-primary);">
check_circle
</span>
<div>
<div style="color: var(--md-sys-color-primary); font-size: 0.875rem; font-weight: 500;" th:text="#{adminUserSettings.activeUsers}">Active Users</div>
<div style="color: var(--md-sys-color-primary); font-size: 1.5rem; font-weight: 700;" th:text="${activeUsers}"></div>
</div>
</div>
<div style="display: flex; align-items: center; gap: 0.75rem;">
<span class="material-symbols-rounded" style="font-size: 2.25rem; color: var(--md-sys-color-primary);">
person_off
</span>
<div>
<div style="color: var(--md-sys-color-primary); font-size: 0.875rem; font-weight: 500;" th:text="#{adminUserSettings.disabledUsers}">Disabled Users</div>
<div style="color: var(--md-sys-color-primary); font-size: 1.5rem; font-weight: 700;" th:text="${disabledUsers}"></div>
</div>
</div>
</div>
</div>
</div>
<!-- Alert Messages -->
<div th:if="${addMessage}" th:class="${#strings.contains(addMessage, 'Successfully') or #strings.contains(addMessage, 'Created') ? 'alert alert-success data-mb-3' : 'alert alert-danger data-mb-3'}">
<span th:text="#{${addMessage}}">Default message if not found</span>
</div>
<div th:if="${changeMessage}" th:class="${#strings.contains(changeMessage, 'Successfully') or #strings.contains(changeMessage, 'Created') ? 'alert alert-success data-mb-3' : 'alert alert-danger data-mb-3'}">
<span th:text="#{${changeMessage}}">Default message if not found</span>
</div>
<div th:if="${deleteMessage}" th:class="${#strings.contains(deleteMessage, 'Successfully') or #strings.contains(deleteMessage, 'Created') ? 'alert alert-success data-mb-3' : 'alert alert-danger data-mb-3'}">
<span th:text="#{${deleteMessage}}">Default message if not found</span>
</div>
<!-- Admin Actions -->
<div class="data-section-title">User Management</div>
<div class="data-actions data-mb-3">
<button
th:data-bs-toggle="${@runningProOrHigher && totalUsers >= maxPaidUsers} ? null : 'modal'"
th:data-bs-target="${@runningProOrHigher && totalUsers >= maxPaidUsers} ? null : '#addUserModal'"
th:class="${@runningProOrHigher && totalUsers >= maxPaidUsers} ? 'data-btn data-btn-danger' : 'data-btn data-btn-primary'"
th:title="${@runningProOrHigher && totalUsers >= maxPaidUsers} ? #{adminUserSettings.maxUsersReached} : #{adminUserSettings.addUser}">
<span class="material-symbols-rounded">person_add</span>
<span th:text="#{adminUserSettings.addUser}">Add New User</span>
</button>
<a th:href="@{'/teams'}" class="data-btn data-btn-secondary" th:title="#{adminUserSettings.teams}">
<span class="material-symbols-rounded">group</span>
<span th:text="#{adminUserSettings.teams}">Manage Teams</span>
</a>
<button
data-bs-toggle="modal"
data-bs-target="#changeUserRoleModal"
class="data-btn data-btn-secondary"
th:title="#{adminUserSettings.changeUserRole}">
<span class="material-symbols-rounded">edit</span>
<span th:text="#{adminUserSettings.changeUserRole}">Change User's Role</span>
</button>
<a th:href="@{'/usage'}" th:if="${@runningEE}" class="data-btn data-btn-secondary" th:title="#{adminUserSettings.usage}">
<span class="material-symbols-rounded">analytics</span>
<span th:text="#{adminUserSettings.usage}">Usage Statistics</span>
</a>
<a href="/audit" th:if="${@runningEE}" class="data-btn data-btn-secondary" title="Audit Dashboard">
<span class="material-symbols-rounded">security</span>
<span>Audit Dashboard</span>
</a>
</div>
<!-- Users Table -->
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col" th:title="#{username}" class="text-overflow" th:text="#{username}">Username</th>
<th scope="col" th:title="#{adminUserSettings.team}" class="text-overflow" th:text="#{adminUserSettings.team}">Team</th>
<th scope="col" th:title="#{adminUserSettings.role}" class="text-overflow" th:text="#{adminUserSettings.role}">Roles</th>
<th scope="col" th:title="#{adminUserSettings.authenticated}" class="text-overflow" th:text="#{adminUserSettings.authenticated}">Authenticated</th>
<th scope="col" th:title="#{adminUserSettings.lastRequest}" class="text-overflow" th:text="#{adminUserSettings.lastRequest}">Last Request</th>
<th scope="col" th:title="#{adminUserSettings.actions}" class="text-overflow" th:text="#{adminUserSettings.actions}">Actions</th>
</tr>
</thead>
<tbody>
<tr th:each="user : ${users}">
<td th:text="${user.id}" th:title="${user.id}" class="text-overflow"></td>
<td th:text="${user.username}" th:title="${user.username}" class="text-overflow" th:classappend="${userSessions[user.username] ? 'active-user' : ''}"></td>
<td th:text="${user.team != null ? user.team.name : '—'}" th:title="${user.team != null ? user.team.name : '—'}" class="text-overflow"></td>
<td>
<span class="data-badge" style="background-color: var(--md-sys-color-secondary-container); color: var(--md-sys-color-secondary); padding: 0.25rem 0.5rem; border-radius: 1rem; font-size: 0.875rem; display: inline-flex; align-items: center; gap: 0.25rem;">
<span class="material-symbols-rounded" style="font-size: 1rem;">shield</span>
<span th:text="#{${user.roleName}}" th:title="#{${user.roleName}}" class="text-overflow">Role</span>
</span>
</td>
<td th:text="${user.authenticationType}" th:title="${user.authenticationType}"></td>
<td th:text="${userLastRequest[user.username] != null ? #dates.format(userLastRequest[user.username], 'yyyy-MM-dd HH:mm:ss') : 'N/A'}" th:title="${userLastRequest[user.username] != null ? #dates.format(userLastRequest[user.username], 'yyyy-MM-dd HH:mm:ss') : 'N/A'}"></td>
<td>
<div class="data-action-cell">
<form th:if="${user.username != currentUsername}" th:action="@{'/api/v1/user/admin/deleteUser/' + ${user.username}}" method="post" onsubmit="return confirmDeleteUser()" style="display: inline;">
<button type="submit" th:title="#{adminUserSettings.deleteUser}" class="data-icon-btn data-icon-btn-danger">
<span class="material-symbols-rounded">person_remove</span>
</button>
</form>
<a th:if="${user.username == currentUsername}" th:title="#{adminUserSettings.editOwnProfil}" th:href="@{'/account'}" class="data-icon-btn data-icon-btn-primary">
<span class="material-symbols-rounded">edit</span>
</a>
<form th:action="@{'/api/v1/user/admin/changeUserEnabled/' + ${user.username}}" method="post" onsubmit="return confirmChangeUserStatus()" style="display: inline;">
<input type="hidden" name="enabled" th:value="!${user.enabled}" />
<button type="submit" th:if="${user.enabled}" th:title="#{adminUserSettings.enabledUser}" class="data-icon-btn data-icon-btn-primary">
<span class="material-symbols-rounded">person</span>
</button>
<button type="submit" th:unless="${user.enabled}" th:title="#{adminUserSettings.disabledUser}" class="data-icon-btn data-icon-btn-danger">
<span class="material-symbols-rounded">person_off</span>
</button>
</form>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<p th:if="${!@runningProOrHigher}" class="data-mt-3" th:text="#{enterpriseEdition.ssoAdvert}"></p>
</div>
</div>
</div>
</div>
<!-- Change User Role Modal -->
<div class="modal fade" id="changeUserRoleModal" tabindex="-1" aria-labelledby="changeUserRoleModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<form th:action="@{'/api/v1/user/admin/changeRole'}" method="post" class="modal-content data-modal">
<div class="data-modal-header">
<h5 class="data-modal-title">
<span class="data-icon">
<span class="material-symbols-rounded">edit</span>
</span>
<span th:text="#{adminUserSettings.changeUserRole}">Change User's Role</span>
</h5>
<button type="button" class="data-btn-close" data-bs-dismiss="modal" aria-label="Close">
<span class="material-symbols-rounded">close</span>
</button>
</div>
<div class="data-modal-body">
<div class="data-mb-2">
<button class="data-btn data-btn-secondary" data-toggle="tooltip" data-placement="auto" th:title="#{downgradeCurrentUserLongMessage}" style="padding: 0.25rem 0.5rem;">
<span class="material-symbols-rounded">help</span>
<span th:text="#{help}">Help</span>
</button>
</div>
<div class="data-form-group">
<label for="username" class="data-form-label" th:text="#{username}">Username</label>
<select name="username" id="username" class="data-form-control" required>
<option value="" disabled selected th:text="#{selectFilter}">-- Select --</option>
<option th:each="user : ${users}" th:if="${user.username != currentUsername}" th:value="${user.username}" th:text="${user.username}">Username</option>
</select>
</div>
<div class="data-form-group">
<label for="role" class="data-form-label" th:text="#{adminUserSettings.role}">Role</label>
<select name="role" id="role" class="data-form-control" required>
<option value="" disabled selected th:text="#{selectFilter}">-- Select --</option>
<option th:each="roleDetail : ${roleDetails}" th:value="${roleDetail.key}" th:text="#{${roleDetail.value}}">Role</option>
</select>
</div>
<div class="data-form-group">
<label for="team" class="data-form-label" th:text="#{adminUserSettings.team}">Team</label>
<select name="teamId" id="team" class="data-form-control" required>
<option value="" th:text="#{selectFilter}">-- Select --</option>
<option th:each="team : ${teams}" th:value="${team.id}" th:text="${team.name}"></option>
</select>
</div>
<div class="data-form-actions">
<button type="button" class="data-btn data-btn-secondary" data-bs-dismiss="modal">
<span class="material-symbols-rounded">close</span>
<span th:text="#{cancel}">Cancel</span>
</button>
<button type="submit" class="data-btn data-btn-primary">
<span class="material-symbols-rounded">check</span>
<span th:text="#{adminUserSettings.submit}">Save User</span>
</button>
</div>
</div>
</form>
</div>
</div>
<!-- Add User Modal -->
<div class="modal fade" id="addUserModal" tabindex="-1" style="z-index: 10000;" aria-labelledby="addUserModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<form id="formsaveuser" th:action="@{'/api/v1/user/admin/saveUser'}" method="post" class="modal-content data-modal">
<div class="data-modal-header">
<h5 class="data-modal-title">
<span class="data-icon">
<span class="material-symbols-rounded">person_add</span>
</span>
<span th:text="#{adminUserSettings.addUser}">Add New User</span>
</h5>
<button type="button" class="data-btn-close" data-bs-dismiss="modal" aria-label="Close">
<span class="material-symbols-rounded">close</span>
</button>
</div>
<div class="data-modal-body">
<div class="data-mb-2">
<button class="data-btn data-btn-secondary" data-toggle="tooltip" data-placement="auto" th:title="#{adminUserSettings.usernameInfo}" style="padding: 0.25rem 0.5rem;">
<span class="material-symbols-rounded">help</span>
<span th:text="#{help}">Help</span>
</button>
</div>
<div class="data-form-group">
<label for="username" class="data-form-label" th:text="#{username}">Username</label>
<input type="text" class="data-form-control" name="username" id="username" th:title="#{adminUserSettings.usernameInfo}" required>
<span id="usernameError" style="display: none; color: var(--md-sys-color-error);" th:text="#{invalidUsernameMessage}">Invalid username!</span>
</div>
<div class="data-form-group" id="passwordContainer">
<label for="password" class="data-form-label" th:text="#{password}">Password</label>
<input type="password" class="data-form-control" name="password" id="password" required>
</div>
<div class="data-form-group">
<label for="role" class="data-form-label" th:text="#{adminUserSettings.role}">Role</label>
<select name="role" class="data-form-control" id="role" required>
<option value="" disabled selected th:text="#{selectFilter}">-- Select --</option>
<option th:each="roleDetail : ${roleDetails}" th:value="${roleDetail.key}" th:text="#{${roleDetail.value}}">Role</option>
</select>
</div>
<div class="data-form-group">
<label for="team" class="data-form-label" th:text="#{adminUserSettings.team}">Team</label>
<select name="teamId" class="data-form-control" required>
<option value="" th:text="#{selectFilter}">-- Select --</option>
<option th:each="team : ${teams}" th:value="${team.id}" th:text="${team.name}"></option>
</select>
</div>
<div class="data-form-group">
<label for="authType" class="data-form-label">Authentication Type</label>
<select id="authType" name="authType" class="data-form-control" required>
<option value="web" selected>WEB</option>
<option value="sso">SSO</option>
</select>
</div>
<div class="data-form-group" id="checkboxContainer">
<div class="form-check">
<input id="forceChange" name="forceChange" type="checkbox">
<label for="forceChange" th:text="#{adminUserSettings.forceChange}">Force user to change username/password on login</label>
</div>
</div>
<div class="data-form-actions">
<button type="button" class="data-btn data-btn-secondary" data-bs-dismiss="modal">
<span class="material-symbols-rounded">close</span>
<span th:text="#{cancel}">Cancel</span>
</button>
<button type="submit" class="data-btn data-btn-primary">
<span class="material-symbols-rounded">check</span>
<span th:text="#{adminUserSettings.submit}">Save User</span>
</button>
</div>
</div>
</form>
</div>
</div>
<!-- Add Team Modal -->
<div class="modal fade" id="addTeamModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<form th:action="@{'/api/v1/team/create'}" method="post" class="modal-content data-modal">
<div class="data-modal-header">
<h5 class="data-modal-title">
<span class="data-icon">
<span class="material-symbols-rounded">group_add</span>
</span>
<span th:text="#{adminUserSettings.createTeam}">Create Team</span>
</h5>
<button type="button" class="data-btn-close" data-bs-dismiss="modal" aria-label="Close">
<span class="material-symbols-rounded">close</span>
</button>
</div>
<div class="data-modal-body">
<div class="data-form-group">
<label for="teamName" class="data-form-label" th:text="#{adminUserSettings.teamName}">Team Name</label>
<input type="text" name="name" id="teamName" class="data-form-control" required />
</div>
<div class="data-form-actions">
<button type="button" class="data-btn data-btn-secondary" data-bs-dismiss="modal">
<span class="material-symbols-rounded">close</span>
<span th:text="#{cancel}">Cancel</span>
</button>
<button type="submit" class="data-btn data-btn-primary">
<span class="material-symbols-rounded">check</span>
<span th:text="#{adminUserSettings.submit}">Create</span>
</button>
</div>
</div>
</form>
</div>
</div>
<script th:inline="javascript">
const delete_confirm_text = /*[[#{adminUserSettings.confirmDeleteUser}]]*/ 'Should the user be deleted?';
const change_confirm_text = /*[[#{adminUserSettings.confirmChangeUserStatus}]]*/ 'Should the user be disabled/enabled?';
function confirmDeleteUser() {
return confirm(delete_confirm_text);
}
function confirmChangeUserStatus() {
return confirm(change_confirm_text);
}
jQuery.validator.addMethod("usernamePattern", function(value, element) {
// Regular expression for user name: Min. 3 characters, max. 50 characters
const regexUsername = /^[a-zA-Z0-9](?!.*[-@._+]{2,})([a-zA-Z0-9@._+-]{1,48})[a-zA-Z0-9]$/;
// Regular expression for email addresses: Max. 320 characters, with RFC-like validation
const regexEmail = /^(?=.{1,320}$)(?=.{1,64}@)[A-Za-z0-9](?:[A-Za-z0-9_.+-]*[A-Za-z0-9])?@[^-][A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*(?:\.[A-Za-z]{2,})$/;
// Check if the field is optional or meets the requirements
return this.optional(element) || regexUsername.test(value) || regexEmail.test(value);
}, /*[[#{invalidUsernameMessage}]]*/ "Invalid username format");
$(document).ready(function() {
$('[data-toggle="tooltip"]').tooltip();
$('#formsaveuser').validate({
rules: {
username: {
required: true,
usernamePattern: true
},
password: {
required: true
},
role: {
required: true
},
authType: {
required: true
}
},
messages: {
username: {
usernamePattern: /*[[#{invalidUsernameMessage}]]*/ "Invalid username format"
},
},
errorPlacement: function(error, element) {
if (element.attr("name") === "username") {
$("#usernameError").text(error.text()).show();
} else if (element.attr("name") !== "role" && element.attr("name") !== "authType") {
error.insertAfter(element);
}
},
success: function(label, element) {
if ($(element).attr("name") === "username") {
$("#usernameError").hide();
}
}
});
$('#username').on('input', function() {
var usernameInput = $(this);
var isValid = usernameInput[0].checkValidity();
var errorSpan = $('#usernameError');
if (isValid) {
usernameInput.removeClass('invalid').addClass('valid');
errorSpan.hide();
} else {
usernameInput.removeClass('valid').addClass('invalid');
errorSpan.show();
}
});
$('#authType').on('change', function() {
var authType = $(this).val();
var passwordField = $('#password');
var passwordFieldContainer = $('#passwordContainer');
var checkboxContainer = $('#checkboxContainer');
if (authType === 'sso') {
passwordField.removeAttr('required');
passwordField.prop('disabled', true).val('');
passwordFieldContainer.slideUp('fast');
checkboxContainer.slideUp('fast');
} else {
passwordField.prop('disabled', false);
passwordField.attr('required', 'required');
passwordFieldContainer.slideDown('fast');
checkboxContainer.slideDown('fast');
}
});
});
</script>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>

Some files were not shown because too many files have changed in this diff Show More