Compare commits

...
269 Commits
Author SHA1 Message Date
Rafael Roseira Machado 47cad0a131 fix pause-rounded icon typos and comments (#5992) 2026-03-24 18:56:51 +00:00
stirlingbot[bot]andAnthony Stirling 4858608162 🤖 format everything with pre-commit by stirlingbot (#5946)
Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2026-03-24 18:55:37 +00:00
OUNZAR AymaneandCopilot a1f03c844b Enhance multi-page PDF layout with advanced customization options (#397, #3655) (#5859)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-24 17:27:56 +00:00
InstaZDLLandAnthony Stirling 8bbfbd63d7 feat(security): add RFC 3161 PDF timestamp tool (#5855)
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2026-03-24 17:00:33 +00:00
Anthony Stirling 7b3985e34a FileReadiness (#5985) 2026-03-24 15:25:33 +00:00
Anthony Stirling f03f0d4adb junits (#5988) 2026-03-24 14:12:31 +00:00
Anthony Stirling c3fc200c5d Remove images (#5966) 2026-03-24 14:11:27 +00:00
briosandReece Browne c3530024c4 feat(pdf): replace PdfLib with Pdfium for form handling and general rendering tasks (#5899)
# Description of Changes

Improves PDF rendering in the viewer by adding digital signature field
support,
cleaning up overlay rendering, and migrating the contrast tool off
pdf-lib to PDFium WASM.

### Signature Field Overlay
- Added `SignatureFieldOverlay` component that renders digital signature
form fields
- Renders appearance streams when present; shows a fallback badge for
unsigned fields
- Uses PDFium WASM for bitmap extraction

### Overlay Rendering
- Integrated `SignatureFieldOverlay` and `ButtonAppearanceOverlay` into
`LocalEmbedPDF`
- Overlays are now clipped to page boundaries
- Clarified in `EmbedPdfViewer` that frontend overlays use PDFium WASM,
  backend overlays use PDFBox

### Contrast Tool Migration
- Replaced pdf-lib with PDFium WASM in `useAdjustContrastOperation`
- PDF page creation and image embedding now go through PDFium APIs
directly
- Updated bitmap handling and memory management accordingly

### Cleanup
- Fixed import ordering in viewer components
- Removed stale comments in the contrast operation hook

<!--
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: Reece Browne <74901996+reecebrowne@users.noreply.github.com>
2026-03-24 13:34:52 +00:00
Reece Browne 3ea11352e3 Fix/v2/text selection 2 (#5990) 2026-03-24 12:51:52 +00:00
brios 1276e5675e chore(deps): bump pdfbox version to 3.0.7 (#5923) 2026-03-23 19:44:05 +00:00
dependabot[bot] 81c4718954 build(deps): bump sigstore/cosign-installer from 4.0.0 to 4.1.0 (#5975)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-23 19:40:01 +00:00
dependabot[bot] 1806b5d3be build(deps): bump actions/cache from 5.0.3 to 5.0.4 (#5976)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-23 19:38:07 +00:00
dependabot[bot] 81c0187bf1 build(deps): bump softprops/action-gh-release from 2.5.0 to 2.6.1 (#5979)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-23 19:37:37 +00:00
dependabot[bot] 9d51414fbb build(deps): bump docker/setup-qemu-action from 3.7.0 to 4.0.0 (#5977)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-23 19:37:06 +00:00
EthanHealy01andClaude Sonnet 4.6 2e2b55e87d Desktop/remove hard requirement auth wall on desktop (#5956)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 19:36:48 +00:00
ConnorYoh 081b1ec49e Invite-link-issues (#5983) 2026-03-23 19:35:41 +00:00
c46156f37f Bump/embed pdfv2.8.0 (#5921)
please merge #5919, alternatively, just push this and delete that PR
because this is a continuation of that.

This PR bumps the embed PDF version to 2.8.0 and also adds comments
functionaliy

---------

Co-authored-by: ConnorYoh <40631091+ConnorYoh@users.noreply.github.com>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2026-03-23 14:35:39 +00:00
Reece Browne 41945543e0 Fix save converted files (#5971)
Fix saving converted files on tauri
2026-03-23 13:51:40 +00:00
James Brunton e5f6180dbe Remove cmd-r override for rotation because it interferes with refresh (#5981)
# Description of Changes
Currently, cmd-r is set to rotate the PDF in the viewer instead of
perform refresh in the browser. This is unintuitive and confusing for
Mac users, and for Windows users (who are less used to doing ctrl-r for
refresh) it only works some of the time, if the Viewer is active, so
removing the override is no great loss.
2026-03-23 13:26:10 +00:00
James Brunton 57c810ab9a Add frontend developer guide describing the path alias architecture (#5964)
# Description of Changes
Add frontend developer guide describing the path alias architecture.
There's probably more needed in here which we should flesh out over
time, but this is a start.
2026-03-23 10:16:52 +00:00
brios b012f18a40 fix(gradle): bump gradle jar version to 9.3.1-bin (#5938) 2026-03-20 12:00:01 +00:00
Anthony Stirling 9e8606cab4 XSS for eml and others (#5967) 2026-03-20 11:55:23 +00:00
Achieve3318andJames Brunton 55bcb92810 Add explicit Save As button for desktop viewer (issue #5928) (#5959)
## Description

Adds an explicit **“Save As”** button to the desktop viewer so users can
always save a copy of the current PDF to a different location, even if
the original file already has a local path.

This complements the existing smart **Save/Download** behavior:
- The existing download button continues to either save back to the
original path (when available) or prompt for a path when needed.
- The new **Save As** button always opens a save dialog to choose a
location/name for a new copy.

## Changes

- **RightRail (viewer controls)**
- Added a new **Save As** action icon in the right rail settings
section.
  - The button:
- Uses `viewerContext.exportActions.saveAsCopy()` to get the current
viewer state as a PDF.
- Calls `downloadFile` without a `localPath`, ensuring the desktop app
shows a **Save As** dialog.
- Picks the first selected file (if any) or the first active file as the
source for the filename.
- **Desktop / Web behavior**
  - In the desktop app (Tauri), clicking **Save As**:
- Opens a native save dialog so the user can choose a different folder
and filename.
- Writes a new copy without changing the existing file’s `localFilePath`
or dirty state.
- In the web app, the button behaves like a standard download of a copy
(browser-controlled save dialog / download).

## Motivation

- Users often want to apply operations on a PDF while **keeping the
original unmodified**.
- The existing smart Save behavior chooses between Save and Save As
automatically, but there was no way to explicitly request **Save As**.
- This change gives desktop users a clear, dedicated **“Save As”**
control while preserving the current Save/Download behavior.

## Notes

- No backend changes.
- No changes to the existing Save / Download button behavior.
- The new button uses existing viewer export and download utilities,
minimizing new logic.

---------

Co-authored-by: James Brunton <james@stirlingpdf.com>
2026-03-20 09:32:24 +00:00
Aarón Rosa Díaz a7f2abcb22 Update Spanish translation (translation.toml) (#5965) 2026-03-19 17:15:42 +00:00
Anthony Stirling 3376a87f15 speaking! (#5925) 2026-03-19 14:11:36 +00:00
PandaMan 2b9f03237a Fix non-ASCII characters in headers being rejected (#5377) (#5699) 2026-03-17 19:23:18 +00:00
ConnorYoh 214dc20c2e Hotfix-cant-run-tools-when-no-credits (#5955)
Tested:
* Can sign in on saas -> can run local tools with or without credits->
can run saas only tools (if credits) -> can't run saas only tools
without credits
* Can sign in self-hosted -> can run all tools on remote if available ->
can run local when self-hosted unavailable

Clouds show on saas tools when connected
Tools are disabled when connected to self-hosted but cannot find server.
You also get banner


#cantwaitforplaywritetests
2026-03-17 13:01:08 +00:00
unlair b656e1e2d1 Fix Docker builds on Debian (#5936) 2026-03-16 22:22:16 +00:00
James BruntonandAnthony Stirling 7f9bbebe5b Unify creditCosts.ts files (#5952)
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2026-03-16 22:05:02 +00:00
James Brunton dbff05814f Fix any type usage in the saas/ folder (#5934)
# Description of Changes
Ages ago I made #4835 to try and fix all the `any` type usage in the
system but never got it finished, and there were just too many to review
and ensure it still worked. There's even more now.

My new tactic is to fix folder by folder. This fixes the `any` typing in
the `saas/` folder, and also enables `no-unnecessary-type-assertion`,
which really helps reduce pointless `as` casts that AI generates when
the type is already known. I hope to expand both of these to the rest of
the folders soon, but one folder is better than none.
2026-03-16 11:51:16 +00:00
Rafael Roseira Machado 1722733802 fix jumping cursor bug (#5937) 2026-03-16 11:44:23 +00:00
dependabot[bot] 85d5bb5dc2 build(deps): bump actions/upload-artifact from 6.0.0 to 7.0.0 (#5939)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-16 11:15:15 +00:00
dependabot[bot] 2e64d7cca6 build(deps): bump dorny/paths-filter from 3.0.2 to 4.0.1 (#5943)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-16 11:15:12 +00:00
dependabot[bot] 3908e258c8 build(deps): bump github/codeql-action from 4.32.4 to 4.32.6 (#5941)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-16 11:14:49 +00:00
dependabot[bot] 9b5714277a build(deps): bump srvaroa/labeler from 1.13.0 to 1.14.0 (#5942)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-16 11:14:29 +00:00
dependabot[bot] 9df4692648 build(deps): bump actions/cache from 4.3.0 to 5.0.3 (#5940)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-16 11:14:10 +00:00
James Brunton c58a6092ec Add SaaS AI engine (#5907) 2026-03-16 11:01:50 +00:00
Anthony Stirling cddc8e6df0 Delete code from invalid license (#5947) 2026-03-16 11:01:31 +00:00
James Brunton 971321fb19 Fix printing on Mac desktop (#5920)
# Description of Changes
Fix #5164 

As I mentioned on the bug
https://github.com/Stirling-Tools/Stirling-PDF/issues/5164#issuecomment-4045170827,
it's impossible to print on Mac currently because
`iframe.contentWindow?.print()` silently does nothing in Tauri on Mac,
but [it seems unlikely that this will be
fixed](https://github.com/tauri-apps/tauri/issues/13451#issuecomment-4048075861).

Instead, I've linked directly to the Mac `PDFKit` framework in Rust to
use its printing functionality instead of Safari's. I believe that
`PDFKit` is what `Preview.app` is using and the print UI that it
generates seems to perform identically, so this should solve the issue
on Mac. Hopefully one day the TS iframe print API will be fixed and
we'll be able to get rid of this code, or [there'll be an official Tauri
plugin for printing which we can use
instead](https://github.com/tauri-apps/plugins-workspace/issues/293).

This implementation should be entirely Mac-specific. Windows & Linux
will continue to use their TS printing (which comes from EmbedPDF)
unless we have a good reason to change them to use a native solution as
well.
2026-03-16 10:49:45 +00:00
Balázs Szücs f384e765fb feat(http2): add jetty-alpn-java-server dependency for HTTP/2 support (#5945) 2026-03-15 20:10:34 +00:00
dependabot[bot] 400ee16e83 build(deps): bump actions/download-artifact from 7.0.0 to 8.0.0 (#5887)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-13 19:56:25 +00:00
dependabot[bot] f777efdd1c build(deps): bump actions/setup-python from 6.1.0 to 6.2.0 (#5886)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-13 19:55:26 +00:00
dependabot[bot] 1d62f7ec23 build(deps): bump docker/metadata-action from 5.10.0 to 6.0.0 (#5889)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-13 19:55:12 +00:00
dependabot[bot] c5b202f2a1 build(deps): bump crazy-max/ghaction-github-runtime from 3.1.0 to 4.0.0 (#5890)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-13 19:54:58 +00:00
dependabot[bot]andAnthony Stirling a2b0d1122c build(deps): bump step-security/harden-runner from 2.14.0 to 2.15.1 (#5896)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2026-03-13 15:39:07 +00:00
stirlingbot[bot] 34c629dcb4 Update Backend 3rd Party Licenses (#5930)
Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com>
2026-03-13 14:32:54 +00:00
stirlingbot[bot]andAnthony Stirling 4726f42030 Update Backend 3rd Party Licenses (#5798)
Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2026-03-13 14:29:27 +00:00
EthanHealy01 c9d693f1eb Improve annotations (#5919)
* Text box/notes movement improvements 
* Fix the issue where hiding, then showing annotations looses progress 
* Fix the issue where hidig/showing annotations jumps you back up to the
top of your open document 
* Support ctrl+c and  ctrl+v and backspace to delete 
* Better handling when moving to different tool from annotate 
* Added a color picker eyedropper button  
* Auto-switch to Select after note/text placement, so users can quickly
place and type 
2026-03-13 14:03:27 +00:00
ConnorYoh 44e036da5a Check if saas before blocking credit insufficiencies (#5929)
fixes #5926
2026-03-13 10:28:39 +00:00
albanobattistella 9969fe5a6d Update Italian translations (#5884) 2026-03-12 20:27:03 +00:00
ConnorYohandAnthony Stirling 0545c3f997 Cleanup-conversion-translations (#5906)
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2026-03-12 20:23:13 +00:00
Reece Browne b68b406a2a Fix rotate failing on large documents (#5917) 2026-03-12 17:57:43 +00:00
James Brunton 8674765528 Add system for managing env vars (#5902)
# Description of Changes
Previously, `VITE_*` environment variables were scattered across the
codebase with hardcoded fallback values inline (e.g.
`import.meta.env.VITE_STRIPE_KEY || 'pk_live_...'`). This made it
unclear which variables
were required, what they were for, and caused real keys to be silently
used in builds where they hadn't been explicitly configured.

## What's changed

I've added `frontend/.env.example` and `frontend/.env.desktop.example`,
which declare every `VITE_*` variable the app uses, with comments
explaining each one and sensible defaults where applicable. These
are the source of truth for what's required.

I've added a setup script which runs before `npm run dev`, `build`,
`tauri-dev`, and all `tauri-build*` commands. It:
- Creates your local `.env` / `.env.desktop` from the example files on
first run, so you don't need to do anything manually
- Errors if you're missing keys that the example defines (e.g. after
pulling changes that added a new variable). These can either be
manually-set env vars, or in your `.env` file (env vars take precedence
over `.env` file vars when running)
- Warns if you have `VITE_*` variables set in your environment that
aren't listed in any example file

I've removed all `|| 'hardcoded-value'` defaults from source files
because they are not necessary in this system, as all variables must be
explicitly set (they can be set to `VITE_ENV_VAR=`, just as long as the
variable actually exists). I think this system will make it really
obvious exactly what you need to set and what's actually running in the
code.

I've added a test that checks that every `import.meta.env.VITE_*`
reference found in source is present in at least one example file, so
new variables can't be added without being documented.

## For contributors

New contributors shouldn't need to do anything - `npm run dev` will
create your `.env` automatically.

If you already have a `.env` file in the `frontend/` folder, you may
well need to update it to make the system happy. Here's an example
output from running `npm run dev` with an old `.env` file:

```
$ npm run dev

> frontend@0.1.0 dev
> npm run prep && vite


> frontend@0.1.0 prep
> tsx scripts/setup-env.ts && npm run generate-icons

setup-env: see frontend/README.md#environment-variables for documentation
setup-env: .env is missing keys from config/.env.example:
  VITE_GOOGLE_DRIVE_CLIENT_ID
  VITE_GOOGLE_DRIVE_API_KEY
  VITE_GOOGLE_DRIVE_APP_ID
  VITE_PUBLIC_POSTHOG_KEY
  VITE_PUBLIC_POSTHOG_HOST
  Add them manually or delete your local file to re-copy from the example.
setup-env: the following VITE_ vars are set but not listed in any example file:
  VITE_DEV_BYPASS_AUTH
  Add them to config/.env.example or config/.env.desktop.example if they are required.
```

If you add a new `VITE_*` variable to the codebase, add it to the
appropriate `frontend/config/.env.example` file or the test will fail.
2026-03-12 13:03:44 +00:00
ConnorYoh d5d03b9ada Manage state of price-lookup calls (#5915)
Now calls stripe-price-lookup once when prices are required rather then
bombarding on every rerender
2026-03-11 13:53:49 +00:00
James Brunton 32cf6866f3 Move AI advice to AGENTS.md and add symlink from CLAUDE.md (#5914)
# Description of Changes
Inspired by https://github.com/pydantic/pydantic-ai/pull/4169, this PR
moves our `CLAUDE.md` advice to the more generic `AGENTS.md` file (which
works on Codex, Gemini, etc). It also adds a symlink from `CLAUDE.md` to
`AGENTS.md`, which Claude follows properly, so all AIs should get the
same advice and we only need to keep one file up-to-date.
2026-03-11 13:43:30 +00:00
James Brunton fa8c52b2be Add SaaS frontend code (#5879)
# Description of Changes
Adds the code for the SaaS frontend as proprietary code to the OSS repo.
This version of the code is adapted from 22/1/2026, which was the last
SaaS version based on the 'V2' design. This will move us closer to being
able to have the OSS products understand whether the user has a SaaS
account, and provide the correct UI in those cases.
2026-03-11 11:53:54 +00:00
ConnorYoh 8bc37bf5ae Desktop: Fallback to local backend if self-hosted server is offline (#5880)
* Adds a fallback mechanism so the desktop app routes tool operations to
the local bundled backend when the user's self-hosted Stirling-PDF
server goes offline, and disables tools in the UI that aren't supported
locally.

* `selfHostedServerMonitor.ts` independently polls the self-hosted
server every 15s and exposes which tool endpoints are unavailable when
it goes offline
* `operationRouter.ts` intercepts operations destined for the
self-hosted server and reroutes them to the local bundled backend when
the monitor reports it offline
* `useSelfHostedToolAvailability.ts` feeds the offline tool set into
useToolManagement, disabling affected tools in the UI with a
selfHostedOffline reason and banner warning

- `SelfHostedOfflineBanner `is a dismissable (session-only) gray bar
shown at the top of the UI when in self-hosted mode and the server goes
offline. It shows:
2026-03-10 10:04:56 +00:00
James Brunton 6d9fc59bc5 Get rid of bad description for file association on Windows (#5905)
# Description of Changes
Explorer currently shows this on Windows:

<img width="380" height="43" alt="image"
src="https://github.com/user-attachments/assets/a892d827-8e0a-4f85-a035-52a454eaadd8"
/>

This PR just removes the description for the file association so we use
the default behaviour. This string is only used on Windows according to
the [Tauri docs](https://v2.tauri.app/reference/config/#description-1).
2026-03-09 15:02:10 +00:00
ConnorYohandJames Brunton ff31b2f9ca Posthog-fixes (#5901)
PostHog is now initialized with persistence: 'memory' so no cookies are
written on first load. Consent is handled in a PostHogConsentSync
component that switches to localStorage+cookie persistence only when the
user accepts, using the official @posthog/react package (cherry-picked
from 14aaf64)

---------

Co-authored-by: James Brunton <james@stirlingpdf.com>
2026-03-09 12:13:09 +00:00
Brian Banerjee 81596f0299 Limit PostHog cookie to Stirling PDF's subdomain only (#5882) 2026-03-08 21:03:10 +00:00
Reece Browne 63d38e382d Chore/v2/transforms as root (#5868)
Any task that changes file type or produces more/fewer files than the
input are now consumed as root files not incremented versions of the
input.
2026-03-06 13:46:40 +00:00
Anthony Stirling a57e336675 Add searchable settings selector to AppConfigModal and improve nav behavior (#5873) 2026-03-06 11:11:17 +00:00
Anthony Stirling 456106195e translations and version bump (#5878) 2026-03-06 11:09:18 +00:00
Anthony Stirling 7c1eb4183b stop enabling english (#5874) 2026-03-06 10:09:51 +00:00
Anthony StirlingandEthanHealy01 7d640e9ce6 option to hide google drive and add settings (#5863)
Co-authored-by: EthanHealy01 <80844253+EthanHealy01@users.noreply.github.com>
2026-03-06 10:09:33 +00:00
ConnorYoh cafcee6c99 Add the production billing portal link for static plan page (#5860) 2026-03-06 10:08:38 +00:00
ConnorYoh 7fdd100abf Fix signatures not showing (#5872) 2026-03-06 00:43:29 +00:00
dependabot[bot] 086b55b0bb build(deps): bump pypdf from 6.7.4 to 6.7.5 in /testing/cucumber in the pip group across 1 directory (#5853)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-06 00:15:46 +00:00
RenzoandRenzoMXD c77242d943 fix: merge pdf pipeline validation (#5799)
Co-authored-by: RenzoMXD <RenzoMXD@users.noreply.github.com>
2026-03-06 00:14:30 +00:00
dependabot[bot] 30b0924d6b build(deps): bump digicert/ssm-code-signing from 1.2.0 to 1.2.1 (#5692)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-06 00:13:59 +00:00
dependabot[bot] 2d6f206c36 build(deps): bump actions/setup-node from 4.4.0 to 6.2.0 (#5691)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-06 00:13:35 +00:00
Anthony Stirling 6c83da6417 Audit fixes and improvements (#5835) 2026-03-05 22:00:44 +00:00
Anthony Stirling 879ffc066f tauri notifications (#5875) 2026-03-05 18:30:20 +00:00
Anthony Stirling 0f7ee5c5b0 settings menu reworks (#5864) 2026-03-05 16:20:20 +00:00
Anthony Stirling ba2d10a75b Persist Tauri window state between launches (#5871) 2026-03-05 16:18:25 +00:00
ConnorYoh 98835ce7b5 Don't build mac if you don't have the secrets (#5861)
Don't build mac if signing secrets unnavailable. 

No point in trying to build without signing as you cannot install it on
a mac without signature.
2026-03-04 15:59:42 +00:00
stirlingbot[bot] 2f2ced321a 🤖 format everything with pre-commit by stirlingbot (#5775)
Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com>
2026-03-03 21:51:44 +00:00
Anthony Stirling bfe655fecb Bump version from 2.5.3 to 2.6.0 2026-03-03 21:46:31 +00:00
Balázs Szücs 9ac260ee92 feat(aot): add aot-diagnostics.sh for AOT cache diagnostics and validation (#5848)
# Description of Changes



This pull request makes significant improvements to the Docker build
process for the embedded Stirling-PDF image, focusing on build
efficiency, runtime optimization, and maintainability. Key changes
include upgrading major tool versions, introducing optional stripping of
Calibre's WebEngine to reduce image size, consolidating ImageMagick
layers, and refining the Python environment build process. The runtime
image is now leaner, with clearer separation between build and runtime
dependencies, and improved caching for faster builds and pulls.

**Build and Dependency Management Improvements**
* Upgraded Calibre to version `9.4.0` and added support for the
`TARGETPLATFORM` build argument for multi-platform builds.
* Added an optional `CALIBRE_STRIP_WEBENGINE` build argument to strip
Chromium/WebEngine from Calibre, saving ~80 MB when PDF output via
Calibre is not needed.
* Consolidated ImageMagick outputs into a single staging directory
(`/magick-export`) to reduce Docker layers and improve caching
efficiency.
* Refactored Python virtual environment build: now built in a dedicated
stage with pre-built wheels and copied into the runtime image,
eliminating the need for build tools and pip installs at runtime.

**Runtime Image Optimization**
* Reduced installed system packages to only what is needed at runtime;
Python build tools and dev packages are no longer included.
* Cleaned up unnecessary runtime files, including removal of build-only
Python artifacts and system headers, for a smaller and more secure
image.

**Layer and Copy Optimization**
* Switched to `COPY --link` for all major external tool layers and
application files, enabling independent layer caching and parallel pulls
for faster builds.

**Runtime Configuration and Health**
* Improved runtime directory structure and permissions, added persistent
cache directories for Project Leyden AOT, and wrote the version tag to
`/etc/stirling_version` for easier script access.
* Updated the healthcheck to wait longer for startup and increased
timeout/retries for more robust readiness detection.

<!--
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-03-03 19:06:46 +00:00
Anthony Stirling 1b68a513a9 tauri jdk25 and docs (#5814) 2026-03-03 13:49:33 +00:00
Reece BrowneandEthanHealy01 93d7919c4c Fix split tooltips (#5847)
Co-authored-by: EthanHealy01 <80844253+EthanHealy01@users.noreply.github.com>
2026-03-03 13:49:05 +00:00
ConnorYoh 3e4c984fcc Add check for ghostscript before plowing on with removeDataOutsideCrop (#5845) 2026-03-03 12:52:28 +00:00
ConnorYoh c4c43593e6 fallback for /api/v1/config/endpoints-availability (#5842) 2026-03-02 22:03:23 +00:00
fd1b7abc83 refactor(merge,split,json): adopt streaming approach and standardize types, address gradle warnings (#5803)
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
Co-authored-by: Balázs <balazs@heim-041-30.jkh.uni-linz.ac.at>
2026-03-02 21:55:07 +00:00
dependabot[bot]andAnthony Stirling 0c46f77179 build(deps): bump com.sun.xml.bind:jaxb-core from 2.3.0.1 to 4.0.6 (#5365)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2026-03-02 16:16:05 +00:00
ConnorYoh afda066579 Frontend and Desktop audit fixes (#5840) 2026-03-02 15:44:05 +00:00
dependabot[bot] e9148437f6 build(deps-dev): bump stylelint from 16.26.1 to 17.4.0 in /devTools (#5822)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-02 15:23:26 +00:00
dependabot[bot] cb835bcce1 build(deps): bump ajv from 8.17.1 to 8.18.0 in /devTools in the npm_and_yarn group across 1 directory (#5774)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-02 15:17:53 +00:00
dependabot[bot] 161bfef7da build(deps): bump actions/ai-inference from 2.0.5 to 2.0.7 (#5831)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-02 15:14:07 +00:00
dependabot[bot] 690eceb548 build(deps): bump github/codeql-action from 4.31.10 to 4.32.4 (#5833)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-02 15:13:52 +00:00
dependabot[bot] b53c236234 build(deps): bump pypdf from 6.6.2 to 6.7.4 in /testing/cucumber (#5825)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-02 15:12:30 +00:00
Anthony Stirling abb8b1f721 Fix health status checks falling under mettric flag (#5821) 2026-03-02 13:56:51 +00:00
Anthony StirlingandClaude Haiku 4.5 012bd1af92 hardening (#5807)
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-02 13:56:39 +00:00
Anthony Stirling 8b25db37ad fix split cuased by defaultParameters breaking dynamic endpoint tools (#5838) 2026-03-02 13:55:58 +00:00
Balázs Szücs 48dd4154e9 feat(conversion): switch PDF input engine to pdftohtml for improved performance and reduced dependencies (#5820) 2026-03-02 13:55:42 +00:00
StepSecurity Bot cfe040485b [StepSecurity] Apply security best practices (#5830) 2026-03-01 17:16:03 +00:00
Balázs Szücs c15ff1e832 fix(aot): use Spring Boot exploded layer format for aot cache (#5811) 2026-03-01 15:46:02 +00:00
Balázs Szücs c244edf8b7 feat(annotation): add moveAnnotation API for efficient repositioning of annotations, and bump embed to 2.7.0 (#5809) 2026-03-01 15:45:44 +00:00
Anthony Stirling 13d7ee7496 skip certs (#5819)
# 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-02-27 17:31:40 +00:00
Anthony Stirling 930d7a0df8 open-saml bumps (#5805) 2026-02-27 15:03:12 +00:00
Anthony Stirling 6a1597bb8d ci: provide default desktop env vars in tauri GitHub Actions builds (#5815) 2026-02-27 10:57:50 +00:00
aikido-autofix[bot] d98ff194e4 [Aikido] AI Fix for 3rd party Github Actions should be pinned (#5817)
Co-authored-by: aikido-autofix[bot] <119856028+aikido-autofix[bot]@users.noreply.github.com>
2026-02-27 10:42:51 +00:00
Balázs Szücs 7310b75ee6 chore(deps): update dependencies for security (#5813) 2026-02-27 10:10:40 +00:00
Anthony Stirling 1bac8417af Harden shared signature endpoints (#5806) 2026-02-26 12:53:47 +00:00
Anthony Stirling b21e1313d8 docker cache fix (#5801) 2026-02-25 18:35:22 +00:00
Reece Browne 213f136882 lint (#5802) 2026-02-25 18:20:24 +00:00
Anthony Stirling 9438b8db29 DocumentBuilderFactory limiting (#5797) 2026-02-25 17:25:31 +00:00
Anthony Stirling c9e7d9d6c9 deps (#5796) 2026-02-25 15:42:36 +00:00
Anthony StirlingandEthanHealy01 2bacb4dc81 cleanups (#5795)
Co-authored-by: EthanHealy01 <80844253+EthanHealy01@users.noreply.github.com>
2026-02-25 15:19:23 +00:00
James Brunton c9dafc85fd Switch to use ESLint 10 (#5794) 2026-02-25 14:30:40 +00:00
5c39acecd8 Desktop connection SaaS: config, billing, team support (#5768)
Co-authored-by: James Brunton <jbrunton96@gmail.com>
Co-authored-by: James Brunton <james@stirlingpdf.com>
2026-02-25 14:13:07 +00:00
Anthony StirlingandClaude Haiku 4.5 86072ec91a Cachefixing test (#5793)
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
2026-02-25 13:44:38 +00:00
dependabot[bot] c8081ac7cd build(deps): bump pillow from 12.1.0 to 12.1.1 in /testing/cucumber in the pip group across 1 directory (#5719)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-25 11:36:42 +00:00
dependabot[bot]andJames Brunton 9d93f20c39 build(deps-dev): bump pillow from 12.1.0 to 12.1.1 in /.github/scripts in the pip group across 1 directory (#5720)
Bumps the pip group with 1 update in the /.github/scripts directory:
[pillow](https://github.com/python-pillow/Pillow).

Updates `pillow` from 12.1.0 to 12.1.1
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/python-pillow/Pillow/releases">pillow's
releases</a>.</em></p>
<blockquote>
<h2>12.1.1</h2>
<p><a
href="https://pillow.readthedocs.io/en/stable/releasenotes/12.1.1.html">https://pillow.readthedocs.io/en/stable/releasenotes/12.1.1.html</a></p>
<h2>Dependencies</h2>
<ul>
<li>Patch libavif for svt-av1 4.0 compatibility <a
href="https://redirect.github.com/python-pillow/Pillow/issues/9413">#9413</a>
[<a href="https://github.com/hugovk"><code>@​hugovk</code></a>]</li>
</ul>
<h2>Other changes</h2>
<ul>
<li>Fix OOB Write with invalid tile extents <a
href="https://redirect.github.com/python-pillow/Pillow/issues/9427">#9427</a>
[<a
href="https://github.com/radarhere"><code>@​radarhere</code></a>]</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/python-pillow/Pillow/commit/5158d98c807e719c5938aa3886913ef0ea6814e9"><code>5158d98</code></a>
12.1.1 version bump</li>
<li><a
href="https://github.com/python-pillow/Pillow/commit/9000313cc5d4a31bdcdd6d7f0781101abab553aa"><code>9000313</code></a>
Fix OOB Write with invalid tile extents (<a
href="https://redirect.github.com/python-pillow/Pillow/issues/9427">#9427</a>)</li>
<li><a
href="https://github.com/python-pillow/Pillow/commit/cd0111849fb32c40860e3ee3d57b9b1cee4260cf"><code>cd01118</code></a>
Patch libavif for svt-av1 4.0 compatibility</li>
<li>See full diff in <a
href="https://github.com/python-pillow/Pillow/compare/12.1.0...12.1.1">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=pillow&package-manager=pip&previous-version=12.1.0&new-version=12.1.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 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: James Brunton <jbrunton96@gmail.com>
2026-02-25 10:26:40 +00:00
James BruntonandAnthony Stirling eab84a13d0 Change to use dpdm for circular import scanning (#5788)
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2026-02-24 23:05:23 +00:00
Anthony Stirling 1b3bfaec20 Remove push event from multiOSReleases workflow
Removed push event trigger for main and V2-master branches.
2026-02-24 21:02:20 +00:00
Anthony Stirling f7299bf89b disable other dockers (#5792) 2026-02-24 20:37:17 +00:00
Anthony Stirling abbd332909 zip and response issues (#5786) 2026-02-24 20:08:18 +00:00
Balázs SzücsandAnthony Stirling 1f9b90ad57 feat(docker): update base images to Java 25, Spring 4, Jackson 3, Gradle 9 and optimize JVM options (Project Lilliput) (#5725)
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2026-02-24 20:06:32 +00:00
James Brunton 24128dd318 Sync up Tauri versions (#5789) 2026-02-24 18:43:40 +00:00
James Brunton 5b467d19c3 Update frontend minor package versions (#5787) 2026-02-24 14:40:19 +00:00
Balázs SzücsandJames Brunton 6000a2aaed feat(viewer): handle keyboard shortcuts for print, save, undo, etc. (#5748)
Co-authored-by: James Brunton <jbrunton96@gmail.com>
2026-02-23 23:12:23 +00:00
stirlingbot[bot]andAnthony Stirling eaa01a5c23 Update Backend 3rd Party Licenses (#5781)
Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2026-02-23 23:09:43 +00:00
Reece Browne 4f4d93d028 If in viewer load latest file in viewer (#5784) 2026-02-23 22:33:43 +00:00
Reece Browne d3494e3287 Fix export (#5782) 2026-02-23 22:15:32 +00:00
9b0610b2cc feat: split pdf into small chunks by pdfbox (#5718)
Co-authored-by: Ubuntu <ubuntu@vps-1aebde64.vps.ovh.ca>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2026-02-23 21:15:27 +00:00
James Brunton 73213901d1 Fix cookie consent reappearing on desktop builds (#5780)
# Description of Changes
Fix #5779. Cookie consent persistance doesn't work on desktop (on Mac at
least, not sure about Windows) because of permission differences with
Tauri. We are allowed to store things in local storage fine, so this
switches the cookie consent module to store in local storage for
desktop, and leaves it alone for web, where it already worked correctly.
2026-02-23 20:53:31 +00:00
Balázs Szücs 91b4a3484c feat(conversion): add PDF to Excel (XLSX) conversion (#5778) 2026-02-23 20:47:24 +00:00
Balázs Szücs 549f796e47 feat(form-fill): add CSV and XLSX extraction for form fields, improve file ID handling (#5776) 2026-02-23 20:17:58 +00:00
Balázs Szücs 340224b40b refactor(link-layer): migrate to EmbedPDF v2.6.2 annotation state for link rendering and improve link handling (#5760) 2026-02-23 13:34:05 +00:00
Anthony Stirling 30c258ce0b cucumber for days (#5766) 2026-02-21 23:17:28 +00:00
stirlingbot[bot]andAnthony Stirling 7631b222bd 🤖 format everything with pre-commit by stirlingbot (#5675)
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-02-20 22:41:29 +00:00
Anthony Stirling f3f56d1d01 translations ai (please override as you see fit) (#5770)
# 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-02-20 22:36:13 +00:00
Anthony Stirling 83169ed0f4 Move Forms location (#5769)
# 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-02-20 22:35:35 +00:00
albanobattistellaandAnthony Stirling 46d511b8f6 Translate various terms in Italian localization (#5749)
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2026-02-20 21:33:14 +00:00
Hugomaulwurf fd039b8649 Update de translation.toml (#5736)
Vollständige Übersetzung der fehlenden englischen Texte ins Deutsche:
- Schwärzungsfunktionen (Redactions)
- Mobile Upload/Scanner-Einstellungen
- Telegram-Bot-Konfiguration
- Benutzerkontoeinstellungen
- Stempel-Vorlagen und Tooltips
- API-Fehlermeldungen
- Verschiedene UI-Texte

Insgesamt 146 Strings übersetzt.

# 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-02-20 21:31:31 +00:00
Anthony Stirling 72cc500b3c zipFix (#5762)
# 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-02-20 21:27:37 +00:00
Anthony Stirling 9a10fcb590 authclient (#5761)
# 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-02-20 12:08:47 +00:00
Anthony Stirling 7959b3f2a4 licensere reTry (#5763)
# 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-02-20 12:08:20 +00:00
Anthony Stirling dcda01c2b9 dos fixes (#5759)
# 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-02-20 11:13:50 +00:00
LudyandCopilot 1ded9abb46 build(tauri): enforce Java 17+ requirement in Windows jlink build script (#5684)
# Description of Changes

This pull request improves the `scripts/build-tauri-jlink.bat` build
script by adding a check to ensure that Java 17 or higher is installed
before proceeding. This helps prevent build errors due to incompatible
Java versions.

Build process enhancements:

* Added logic to parse the installed Java version and verify that it is
at least version 17; the script now exits with an error message if an
older version is detected.
* Updated status messages to clearly indicate the Java version being
used and to provide feedback if requirements are not met.

---

## 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-02-20 11:12:27 +00:00
Anthony Stirling 031477b7b5 fix publishing for tauri author (#5757)
# 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-02-19 16:11:10 +00:00
Anthony Stirling 8725ba66bb ruler support (#5758)
# Description of Changes

<img width="266" height="228" alt="image"
src="https://github.com/user-attachments/assets/bcc6ec11-bd9e-4b83-a081-62149dd92f2a"
/>

<img width="882" height="335" alt="image"
src="https://github.com/user-attachments/assets/b86dbf13-6bcf-4b28-81a6-8c405358a58e"
/>

<img width="1050" height="399" alt="image"
src="https://github.com/user-attachments/assets/6a4468ed-d0f9-44ab-978a-c640d490da8b"
/>

on hover
<img width="380" height="196" alt="image"
src="https://github.com/user-attachments/assets/ba3755b3-4823-48dc-b6aa-3a0f9b0517a3"
/>

---

## 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-02-19 15:58:03 +00:00
James Brunton 115a24b16d Add plist file for Mac permissions (#5756)
# Description of Changes
There's speculation that the Mac app failing to connect to self-hosted
servers (#5264) is because of missing OS-permissions
[NSLocalNetworkUsageDescription](https://developer.apple.com/documentation/bundleresources/information-property-list/nslocalnetworkusagedescription).
This PR adds it in a `Info.plist` file which ships with the Tauri app
which should throw an OS permissions dialog if required.
2026-02-19 10:26:01 +00:00
Balázs Szücs c775fed17d fix(form-fill): fix hardcoded response (#5755)
# Description of Changes

Closes: #5752

<!--
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-02-18 11:01:58 +00:00
Anthony Stirling ae9d29abf0 large query reduction (#5754)
# Description of Changes

Reduce endpoint-availability call so that an empty param to it returns
all endpoints to avoid pointlessly large http headers


Before:
GET
/api/v1/config/endpoints-availability?endpoints=compress-pdf%2Crotate-pdf%2Cmerge-pdfs%2Csplit-pages%2Cocr-pdf
  for all 74 tools

  After:
  GET /api/v1/config/endpoints-availability
---

## 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-02-18 10:52:59 +00:00
Anthony Stirling ddf93d2b1a fixes for desktop SSO (#5751)
# Description of Changes



Race condition: The browser was opened before the deep-link listener was
registered. On slow first launches the OAuth
callback could arrive before the listener was ready. Fixed by
registering the listener first, then opening the browser
   inside .then() once the listener is confirmed active.

Double-handling: Both SetupWizard and
authService.waitForDeepLinkCompletion processed the same
sso/sso-selfhosted deep
links, calling completeSelfHostedSession and onComplete() independently.
Fixed by moving all SSO completion into
  authService and having SetupWizard defer to it.

Hardening: Removing SetupWizard's handler entirely left no fallback if
the webview reloads while the auth listener's
Promise is in flight. A selfHostedDeepLinkFlowActive flag tracks whether
authService has an active listener.
SetupWizard now acts as a fallback only when the flag is false (i.e.
after a JS context reset), preventing duplicate
  handling in the normal path while preserving resilience on reload.
---

## 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-02-18 10:46:15 +00:00
Balázs Szücs e97f93924e fix(forms): Update form field UI and behavior for better interactivity and alignment (#5747)
# 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-02-18 08:49:52 +00:00
Anthony Stirling 8d5b3eb36b Fix SAML login "something went wrong" when language list = 1 (#5750)
# 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-02-18 08:49:08 +00:00
Reece Browne 757a666f5e Chore/v2/improve annotation UI (#5724) 2026-02-16 22:01:15 +00:00
Anthony Stirling 558c75a2b1 JWT enhancements for desktop (#5742)
# Description of Changes

This is temporary solution which will be enhanced in future

---

## 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-02-16 21:57:42 +00:00
ConnorYoh da2eb54fe8 fix_env_files_for_tauri (#5741)
https://vite.dev/config/#using-environment-variables-in-config
2026-02-16 20:49:23 +00:00
Anthony Stirling 772dd4632e PDF Text editor changes (#5726)
# Description of Changes

 - Reduced lightweight editor JSON size:
- Omit heavy page resources and contentStreams in lazy/lightweight
flows.
      - Omit form fields in lazy metadata/editor bootstrapping flows.
      - Strip inline font program blobs from lazy initial payloads.
  - Added page-based font loading:
      - New endpoint to fetch fonts for a specific cached page:
        GET /api/v1/convert/pdf/text-editor/fonts/{jobId}/{pageNumber}
- Frontend now loads page fonts alongside page data and merges into
local doc state.
  - Reduced save payload duplication:
- Partial export now sends only changed pages (no repeated full-document
font/metadata payload each save).
  - Preserved round-trip/export safety:
- Missing lightweight fields (resources/contentStreams) are interpreted
as “preserve existing from cached PDF.”
- Annotation semantics fixed so explicit empty annotation lists can
clear annotations.
- Fixed a regression where lazy mode could fall back to full export and
lose overlays; lazy now stays on cached
        partial export path when dirty pages exist.
  - Logging/noise reduction
  - Transport optimization:
- Enabled HTTP compression for JSON/problem responses. (might remove
later tho in testing)
      
      
      ### Outcome

  - Much smaller JSON payloads for giant PDFs.
  - Fewer duplicated bytes over the wire.
  - Page-scoped loading of heavy font data.
- Better reliability for preserving overlays/vector/background content
during export.


## 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-02-16 17:36:13 +00:00
Anthony Stirling d5cf77cf50 refactor: fix homepage file upload path (#5738)
Extracts file-based navigation logic from HomePage into pure function
with comprehensive test coverage.

New behavior:
- Opening 1 file from empty → switch to viewer (activeFileIndex: 0)
- Opening 2+ files from empty → switch to fileEditor
- pdfTextEditor tool → no auto-navigation (handles own empty state)
- Non-startup transitions (N→M files) → no navigation

Benefits:
- Pure function → easy to test and reason about
- Clear separation of concerns
- Preserves all existing behavior including pdfTextEditor special case
- Adds new multi-file startup behavior

Changes:
- HomePage.tsx: use getStartupNavigationAction() utility
- homePageNavigation.ts: pure navigation logic
- homePageNavigation.test.ts: comprehensive unit tests

Note: prevFileCountRef initialization kept as useRef(activeFiles.length)
to correctly handle files restored from IndexedDB on app startup.

# 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-02-16 12:40:50 +00:00
Balázs Szücs e310493966 refactor(api): replace regex string literals with Pattern instances for improved performance and readability (#5680)
# 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-02-14 21:01:19 +00:00
Balázs Szücs 0a1d2effdc feat(frontend): Upgrade embedPDF to v2.6.0 and migrate to pdf-lib fork, fix attachment/bookmark panel (#5723)
# Description of Changes

Upgrades embedPDF from v2.5.0 to v2.6.0 and migrates from unmaintained
pdf-lib to @cantoo/pdf-lib fork. Adds defensive error handling for
malformed PDFs and improves bridge lifecycle management.

### Changes

**Dependencies**
- Upgrade all @embedpdf/* packages from ^2.5.0 to ^2.6.0
- Replace pdf-lib with @cantoo/pdf-lib (maintained fork with better
TypeScript support)

**PDF Viewer Infrastructure (attachment/bookmark fix)**
- Add useDocumentReady hook to track document lifecycle across bridges
- Implement defensive bridge cleanup to prevent stale registrations
- Fix race condition in document ready state detection by subscribing to
events before checking state

**Link Extraction (updated to cantoo/pdf-lib)**
- Add graceful error handling for PDFs with invalid catalog structures
- Extract enhanced link metadata (tooltips, colors, border styles,
highlight modes)
- Return empty results instead of throwing on malformed PDFs
- Add validation for link creation (destination page bounds, rect
dimensions, color values)

**Signature Flattening  (updated to cantoo/pdf-lib)**
- Improve SVG embedding with three-tier fallback strategy (native
vector, rasterized PNG, placeholder)
- Add proper Unicode handling for PDF form tooltips via
PDFString.decodeText()
- Extract SVG utilities into cleaner strategy pattern

**Form Field Processing  (updated to cantoo/pdf-lib)**
- Add support for display labels vs export values in dropdown/list
fields per PDF spec 12.7.4.4
- Implement caching for expensive field property lookups
- Add proper handling of malformed /Opt arrays


<!--
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-02-14 20:55:27 +00:00
b8ce4e47c1 Preserve local paths for desktop saves (#5543)
# Summary

- Adds desktop file tracking: local paths are preserved and save buttons
now work as expcted (doing Save/Save As as appropriate)
- Adds logic to track whether files are 'dirty' (they've been modified
by some tool, and not saved to disk yet).
- Improves file state UX (dirty vs saved) and close warnings
- Web behaviour should be unaffected by these changes

## Indicators
Files now have indicators in desktop mode to tell you their state.

### File up-to-date with disk

<img width="318" height="393" alt="image"
src="https://github.com/user-attachments/assets/06325f9a-afd7-4c2f-8a5b-6d11e3093115"
/>

### File modified by a tool but not saved to disk yet

<img width="357" height="385" alt="image"
src="https://github.com/user-attachments/assets/1a7716d9-c6f7-4d13-be0d-c1de6493954b"
/>

### File not tracked on disk

<img width="312" height="379" alt="image"
src="https://github.com/user-attachments/assets/9cffe300-bd9a-4e19-97c7-9b98bebefacc"
/>

# Limitations
- It's a bit weird that we still have files stored in indexeddb in the
app, which are still loadable. We might want to change this behaviour in
the future
- Viewer's Save doesn't persist to disk. I've left that out here because
it'd need a lot of testing to make sure the logic's right with making
sure you can leave the Viewer with applying the changes to the PDF
_without_ saving to disk
- There's no current way to do Save As on a file that has already been
persisted to disk - it's only ever Save. Similarly, there's no way to
duplicate a file.

---------

Co-authored-by: James Brunton <jbrunton96@gmail.com>
Co-authored-by: James Brunton <james@stirlingpdf.com>
2026-02-13 23:15:28 +00:00
Anthony Stirling 946196de43 fix tool disabling for docs and others (#5722)
# 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-02-13 23:15:06 +00:00
Balázs Szücs 27bd34c29b feat(form-fill): FormFill tool with context and UI components for PDF form filling (#5711) 2026-02-13 15:10:48 +00:00
Balázs Szücs 5a1ed50e2b feat(attachments): add attachment support with sidebar and API integration (#5673) 2026-02-13 12:41:15 +00:00
Reece Browne e01734fb7d Fix viewer export (#5713) 2026-02-13 12:16:52 +00:00
Reece Browne 7c3c7937b3 various viewer pill fixes (#5714) 2026-02-13 12:16:30 +00:00
Balázs Szücs b1d44d5661 feat(linklayer): improve link handling with pdf-lib integration and add link toolbar, add delete link functionality (#5715) 2026-02-13 12:16:13 +00:00
Balázs Szücs 71c845bcd8 feat(text-selection): implement text selection enhancement for double and triple-click actions (#5712) 2026-02-13 12:16:01 +00:00
albanobattistellaandLudy c62277a8e5 Update translation (#5670)
Co-authored-by: Ludy <Ludy87@users.noreply.github.com>
2026-02-12 20:04:47 +00:00
Balázs Szücs f3a4dbc903 feat(redaction): improve manual redaction with color selection and updated UI elements (#5679)
# Description of Changes

<img width="1920" height="977" alt="image"
src="https://github.com/user-attachments/assets/17e451b7-df2b-4097-b8aa-66954d89b935"
/>


<!--
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-02-12 19:26:26 +00:00
dependabot[bot]andAnthony Stirling 4b14ddfb37 build(deps): bump com.diffplug.spotless from 8.1.0 to 8.2.1 (#5592)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2026-02-11 23:51:13 +00:00
stirlingbot[bot] 597cc460aa 🌐 Sync Translations + Update README Progress Table (#5668)
Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com>
2026-02-11 23:37:16 +00:00
Balázs Szücs f88f1db7e7 fix(markdown): markdown conversion image handling and zip support (#5677) 2026-02-11 23:31:41 +00:00
Balázs Szücs e523190f39 fix(api): address potential backend resource leaks and improve frontend accessibility (#5678) 2026-02-11 23:31:06 +00:00
Anthony StirlingandConnorYoh f9d2f36ab7 Bug fixing and debugs (#5704)
Co-authored-by: ConnorYoh <40631091+ConnorYoh@users.noreply.github.com>
2026-02-11 18:43:29 +00:00
James Brunton 5df466266a Enhance SSO SAML in desktop app (#5705)
# Description of Changes
Change the SAML support for SSO to understand when a request is coming
from the desktop app, and use the alternate auth flow that the desktop
app requires.
2026-02-11 16:07:06 +00:00
PandaMan cc1931fa75 Fix maxFileSize environment variable support (#5542) (#5655)
## Description

Fixes #5542

This PR adds support for environment variables to configure the file
upload limit, which was previously ignored.

## Changes

- **Added support for `SYSTEMFILEUPLOADLIMIT` environment variable**:
Accepts format like "100MB", "1GB", etc.
- **Added support for `SYSTEM_MAXFILESIZE` environment variable**:
Accepts number in MB (e.g., "100" for 100MB)
- **Initialize `fileUploadLimit` from environment variables**: Added
`@PostConstruct` method in `ApplicationProperties` to read env vars and
set `fileUploadLimit` if not already set in settings.yml
- **Created `MultipartConfiguration`**: New configuration class that
syncs Spring multipart settings with `fileUploadLimit` from settings.yml
or environment variables
- **Updated `application.properties`**: Added documentation about
environment variable support

## How it works

1. On startup,
`ApplicationProperties.initializeFileUploadLimitFromEnv()` checks for
`SYSTEMFILEUPLOADLIMIT` or `SYSTEM_MAXFILESIZE` environment variables
2. If found and `fileUploadLimit` is not set in settings.yml, it sets
the value
3. `MultipartConfiguration` reads the `fileUploadLimit` via
`UploadLimitService` and configures Spring multipart settings
accordingly
4. Users can also still use `SPRING_SERVLET_MULTIPART_MAX_FILE_SIZE` and
`SPRING_SERVLET_MULTIPART_MAX_REQUEST_SIZE` directly

## Testing

Set environment variables:
- `SYSTEMFILEUPLOADLIMIT=10MB` or
- `SYSTEM_MAXFILESIZE=10`

The `fileUploadLimit` in settings.yml should be populated and multipart
limits should be respected.
2026-02-06 23:20:05 +00:00
Anthony Stirlingandstirlingbot[bot] 00a9174939 SSO styling changes (#5671)
# 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: stirlingbot[bot] <stirlingbot[bot]@users.noreply.github.com>
Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com>
2026-02-06 22:00:42 +00:00
stirlingbot[bot]andAnthony Stirling d135e25d02 🤖 format everything with pre-commit by stirlingbot (#5669)
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-02-06 19:58:22 +00:00
Anthony Stirling ba72a2a623 Headless windows installer (#5664) 2026-02-06 18:06:01 +00:00
Anthony Stirling 94e517df3c tauri comments (#5634)
# 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-02-06 13:45:32 +00:00
Ludy c8bc57d31c chore(ci): update GitHub Actions to latest stable versions (#5629)
# Description of Changes

This pull request updates several GitHub Actions and related
dependencies across multiple workflow files to newer versions. The main
goal is to keep the CI/CD pipeline up-to-date with the latest security
patches, features, and bug fixes provided by upstream maintainers. The
changes primarily involve upgrading the versions of commonly used
actions like `actions/checkout`, `docker/login-action`,
`actions/setup-java`, `gradle/actions/setup-gradle`, and
`actions/upload-artifact`.

The most important changes are:

**Actions Version Upgrades (General Maintenance & Security):**
* Upgraded `actions/checkout` from v6.0.1 to v6.0.2 in all workflow
files to ensure the latest bug fixes and improvements are used.
[[1]](diffhunk://#diff-931fcb06ba030420d7044dde06465ad55b4e769a9bd374dcd6a0c76f79a5e30eL119-R119)
[[2]](diffhunk://#diff-931fcb06ba030420d7044dde06465ad55b4e769a9bd374dcd6a0c76f79a5e30eL175-R175)
[[3]](diffhunk://#diff-931fcb06ba030420d7044dde06465ad55b4e769a9bd374dcd6a0c76f79a5e30eL365-R365)
[[4]](diffhunk://#diff-8d23782ae5caff72d55828bb25814854f5f2523f299d7dbcda4a3537dd84c5c3L48-R48)
[[5]](diffhunk://#diff-8d23782ae5caff72d55828bb25814854f5f2523f299d7dbcda4a3537dd84c5c3L136-R136)
[[6]](diffhunk://#diff-8d23782ae5caff72d55828bb25814854f5f2523f299d7dbcda4a3537dd84c5c3L148-R160)
[[7]](diffhunk://#diff-8d23782ae5caff72d55828bb25814854f5f2523f299d7dbcda4a3537dd84c5c3L378-R378)
[[8]](diffhunk://#diff-26fc40a450703e6602af586a24594196fb10e132de14a9a488ae64ee8cc51166L29-R29)
[[9]](diffhunk://#diff-f1e8b4497f902b85c1b990cd7e6ebd928afd9051757fcb8f376be66260c9ea05L26-R26)
[[10]](diffhunk://#diff-cfe84f4bb9657c721ff741644ee0bce45aa81aaef9dea1ea8741c946984e9722L23-R23)
[[11]](diffhunk://#diff-5c3fa597431eda03ac3339ae6bf7f05e1a50d6fc7333679ec38e21b337cb6721L39-R39)
[[12]](diffhunk://#diff-5c3fa597431eda03ac3339ae6bf7f05e1a50d6fc7333679ec38e21b337cb6721L63-R72)
[[13]](diffhunk://#diff-5c3fa597431eda03ac3339ae6bf7f05e1a50d6fc7333679ec38e21b337cb6721L138-R147)
[[14]](diffhunk://#diff-5c3fa597431eda03ac3339ae6bf7f05e1a50d6fc7333679ec38e21b337cb6721L175-R177)
[[15]](diffhunk://#diff-5c3fa597431eda03ac3339ae6bf7f05e1a50d6fc7333679ec38e21b337cb6721L210-R219)
[[16]](diffhunk://#diff-5c3fa597431eda03ac3339ae6bf7f05e1a50d6fc7333679ec38e21b337cb6721L264-R273)
[[17]](diffhunk://#diff-5c3fa597431eda03ac3339ae6bf7f05e1a50d6fc7333679ec38e21b337cb6721L328-R328)
[[18]](diffhunk://#diff-3117b4a93711d37b0a9a1668272eec716fea0b4f57dde16a85e7ab3f569c455dL35-R35)
[[19]](diffhunk://#diff-7cdd3ccec44c8ba176bdc3b9ef54c3f56aa210a1a4e2bb5f79d87b1e50314a18L25-R25)
[[20]](diffhunk://#diff-f8b6ec3c0af9cd2d8dffef6f3def2be6357fe596a606850ca7f5d799e1349069L26-R26)
[[21]](diffhunk://#diff-3c0f521958c53ad27c967692b4d5480ead136acb33622ee97d39df814b1b202eL33-R33)
* Upgraded `docker/login-action` from v3.6.0 to v3.7.0 for improved
Docker Hub authentication.
[[1]](diffhunk://#diff-931fcb06ba030420d7044dde06465ad55b4e769a9bd374dcd6a0c76f79a5e30eL192-R192)
[[2]](diffhunk://#diff-8d23782ae5caff72d55828bb25814854f5f2523f299d7dbcda4a3537dd84c5c3L182-R182)
[[3]](diffhunk://#diff-f8b6ec3c0af9cd2d8dffef6f3def2be6357fe596a606850ca7f5d799e1349069L88-R88)

**Java and Gradle Tooling Updates:**
* Updated `actions/setup-java` from v5.0.0 to v5.2.0 and
`gradle/actions/setup-gradle` from v5.0.0 to v5.0.1 to get the latest
Java and Gradle setup improvements.
[[1]](diffhunk://#diff-8d23782ae5caff72d55828bb25814854f5f2523f299d7dbcda4a3537dd84c5c3L148-R160)
[[2]](diffhunk://#diff-5c3fa597431eda03ac3339ae6bf7f05e1a50d6fc7333679ec38e21b337cb6721L63-R72)
[[3]](diffhunk://#diff-5c3fa597431eda03ac3339ae6bf7f05e1a50d6fc7333679ec38e21b337cb6721L138-R147)
[[4]](diffhunk://#diff-5c3fa597431eda03ac3339ae6bf7f05e1a50d6fc7333679ec38e21b337cb6721L210-R219)
[[5]](diffhunk://#diff-5c3fa597431eda03ac3339ae6bf7f05e1a50d6fc7333679ec38e21b337cb6721L264-R273)
[[6]](diffhunk://#diff-5c3fa597431eda03ac3339ae6bf7f05e1a50d6fc7333679ec38e21b337cb6721L338-R344)
* Upgraded `actions/setup-node` from v5.0.0 to v6.1.0 for Node.js setup.

**Artifact Handling Improvements:**
* Upgraded `actions/upload-artifact` from v4.6.2 to v6.0.0 for uploading
build and test artifacts, which may include performance and reliability
improvements.
[[1]](diffhunk://#diff-5c3fa597431eda03ac3339ae6bf7f05e1a50d6fc7333679ec38e21b337cb6721L104-R104)
[[2]](diffhunk://#diff-5c3fa597431eda03ac3339ae6bf7f05e1a50d6fc7333679ec38e21b337cb6721L160-R160)
[[3]](diffhunk://#diff-5c3fa597431eda03ac3339ae6bf7f05e1a50d6fc7333679ec38e21b337cb6721L193-R193)
[[4]](diffhunk://#diff-5c3fa597431eda03ac3339ae6bf7f05e1a50d6fc7333679ec38e21b337cb6721L232-R232)
[[5]](diffhunk://#diff-5c3fa597431eda03ac3339ae6bf7f05e1a50d6fc7333679ec38e21b337cb6721L379-R379)

**Other Action Updates:**
* Updated `actions/ai-inference` from v2.0.4 to v2.0.5 in the AI PR
title review workflow.

These updates help ensure that the CI/CD workflows remain secure,
reliable, and compatible with the latest tools and platforms.

---

## 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-02-06 11:21:08 +00:00
stirlingbot[bot] e5150df8a5 🤖 format everything with pre-commit by stirlingbot (#5667)
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-02-06 11:20:54 +00:00
stirlingbot[bot] a9faa2aa22 Update Backend 3rd Party Licenses (#5666)
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-02-06 11:20:38 +00:00
Balázs Szücs fb37ee8038 feat(redaction): update to embedPDF v2.4.0 with unified redaction mode support (#5652)
# Description of Changes


Bump to EmbedPDF v2.4.0 plus update to the redaction methods


<!--
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-02-06 11:19:07 +00:00
Ludy 8555fe3fb5 chore(ci): refine pre-commit workflows, add TOML sorting (#5648)
# Description of Changes

This pull request introduces several improvements to pre-commit
configuration and automation, enhances error handling in scripts, and
updates dependencies and exclusions for code quality tools. The main
changes are grouped below:

**Pre-commit and CI workflow improvements:**

* The pre-commit workflow in `.github/workflows/pre_commit.yml` now runs
specific hooks (`ruff`, `ruff-format`, `codespell`, `gitleaks`,
`end-of-file-fixer`, `trailing-whitespace`) individually instead of
running all hooks at once, providing more granular feedback.
* The sync files workflow in `.github/workflows/sync_files_v2.yml` now
installs pre-commit dependencies and runs the `toml-sort-fix` hook to
ensure TOML files are consistently sorted.
* Added the `toml-sort-fix` hook from the `toml-sort` repository to
`.pre-commit-config.yaml` for sorting TOML files in the locales
directory.

**Pre-commit configuration and dependency updates:**

* Updated the `ruff-pre-commit` repository version from `v0.14.8` to
`v0.14.14` in `.pre-commit-config.yaml`.
* Updated the `codespell` hook to expand the ignore words list and to
exclude the `frontend/public/vendor` directory.

**Script improvements and error handling:**

* Replaced bare `except:` clauses with `except Exception:` in
`scripts/convert_cff_to_ttf.py` for safer error handling.
[[1]](diffhunk://#diff-8c68a22370903bb52267848deaf7298604704c59292650d9dfc1d1975fa8bc53L194-R194)
[[2]](diffhunk://#diff-8c68a22370903bb52267848deaf7298604704c59292650d9dfc1d1975fa8bc53L318-R325)
* Minor code cleanup in translation validation scripts by removing
unused variables.
[[1]](diffhunk://#diff-2399f964d817f2e61b818c3f6543ebce9e230778b35ab62bc8578cb7cc9da99eL124)
[[2]](diffhunk://#diff-3b83f838d72dce860ff1f7b24a033f02134aaac3d7abdf061d72c1c21943f896L117)
* Removed unused `progress` variable assignment in
`scripts/counter_translation_v3.py` for clarity.

---

## 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-02-06 11:09:04 +00:00
Ludy 3195f3f2ba chore(ci): improve language TOML check output with fixer guidance (#5638)
# Description of Changes

This pull request enhances the language file difference reporting in
`.github/scripts/check_language_toml.py` by providing clearer guidance
on how to resolve translation key mismatches. The most important changes
are:

**Improved translation key reporting and remediation instructions:**

* When extra keys are detected, the report now suggests the exact
command (`translation_merger.py remove-unused`) to remove them and
provides a direct example for the user.
* When missing keys are found, the report now suggests the exact command
(`translation_merger.py add-missing`) to add them and provides a direct
example for the user.
* For any missing or extra keys, a reference link to the relevant
documentation section is included to help users understand and resolve
the issues.

---

## 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-02-06 11:07:08 +00:00
Ludy 47e0c9cb51 fix(frontend): improve synonym search (#5639)
# Description of Changes

This pull request updates the `getSynonyms` utility function to improve
how it retrieves tool synonyms from translations and enhances logging
for debugging. The function now checks multiple possible translation
keys and provides more detailed log messages to help identify issues
with missing or malformed tags.

Improvements to translation key lookup and logging:

* The function now checks both `home.${toolId}.tags` and
`${toolId}.tags` keys, using the first valid translation found to
increase robustness when fetching synonyms.
* Added detailed logging to indicate which translation key was used and
the resulting tags, making it easier to trace and debug missing or
incorrect translations.
* Improved error handling and cleaned up the function's closing syntax
for clarity and maintainability.

<img width="750" height="703" alt="image"
src="https://github.com/user-attachments/assets/2e89c391-1fc1-4ce1-b171-5ee84aedf37a"
/>


---

## 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-02-06 11:01:37 +00:00
stirlingbot[bot] a23f0ec53d 🌐 Sync Translations + Update README Progress Table (#5618)
### 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-02-06 10:58:03 +00:00
Ludy bf57b1f33b fix(build): define repositories to resolve test classpath dependencies (#5650)
# Description of Changes

## What was changed
- Added an explicit `repositories { ... }` block in the root
`build.gradle` to ensure Gradle has repository definitions for resolving
dependencies.
- Included:
- Optional authenticated Maven repo (only when
`rootProject.ext.mavenUrl` is configured).
  - Shibboleth releases repository.
  - `mavenCentral()` as the default public repository.

## Why the change was made
- Fixes Gradle resolution failure:
- `Cannot resolve external dependency
org.springframework.boot:spring-boot-starter-test because no
repositories are defined.`
- Ensures `:testCompileClasspath` can resolve Spring Boot test
dependencies in all environments, including CI.

---

## 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-02-06 10:50:19 +00:00
Ludy dc6daaad0d feat(desktop): show and reuse last used server URL in Setup Wizard (#5659)
# Description of Changes

This pull request enhances the server selection experience in the setup
wizard by remembering and displaying the last used server URL. Users can
now quickly reuse their previous server connection, improving usability
and reducing repetitive input.

**Server selection improvements:**

* The last used server URL is now stored in `localStorage` and
automatically displayed as a quick-select button in the
`ServerSelection` component, allowing users to easily reconnect to their
previous server.
[[1]](diffhunk://#diff-3a9a7d483f3f7789dbe410067f4401aea5898ad6692755c63e7787585b923151R19-R25)
[[2]](diffhunk://#diff-3a9a7d483f3f7789dbe410067f4401aea5898ad6692755c63e7787585b923151R212-R223)
[[3]](diffhunk://#diff-3a9a7d483f3f7789dbe410067f4401aea5898ad6692755c63e7787585b923151R38)
* A new translation string `useLast` was added to
`frontend/public/locales/en-GB/translation.toml` to support the "Last
used server" button label.

<img width="1282" height="832" alt="image"
src="https://github.com/user-attachments/assets/6f8a6d3a-9f6b-4bcb-9470-da3ad50a4409"
/>

---

## 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-02-06 10:44:42 +00:00
dependabot[bot] 62d2819213 build(deps): bump alpine from 3.23.2 to 3.23.3 in /docker/embedded (#5590)
Bumps alpine from 3.23.2 to 3.23.3.


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=alpine&package-manager=docker&previous-version=3.23.2&new-version=3.23.3)](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-02-06 10:43:59 +00:00
dependabot[bot] 6f217066a1 build(deps): bump org.postgresql:postgresql from 42.7.8 to 42.7.9 (#5554)
[//]: # (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 [org.postgresql:postgresql](https://github.com/pgjdbc/pgjdbc) from
42.7.8 to 42.7.9.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/pgjdbc/pgjdbc/releases">org.postgresql:postgresql's
releases</a>.</em></p>
<blockquote>
<h2>v42.7.9</h2>
<h2>Changes</h2>
<ul>
<li>Added changelogs for version 42.7.9 <a
href="https://github.com/davecramer"><code>@​davecramer</code></a> (<a
href="https://redirect.github.com/pgjdbc/pgjdbc/issues/3908">#3908</a>)</li>
<li>the classloader is nullable, and remove a space <a
href="https://github.com/davecramer"><code>@​davecramer</code></a> (<a
href="https://redirect.github.com/pgjdbc/pgjdbc/issues/3907">#3907</a>)</li>
<li>fix: incorrect pg_stat_replication.reply_time calculation <a
href="https://github.com/atorik"><code>@​atorik</code></a> (<a
href="https://redirect.github.com/pgjdbc/pgjdbc/issues/3906">#3906</a>)</li>
<li>fix: issue <a
href="https://redirect.github.com/pgjdbc/pgjdbc/issues/3892">#3892</a>,
PGXAConnection.prepare(Xid) should return XA_RDONLY if the connection is
read only <a
href="https://github.com/davecramer"><code>@​davecramer</code></a> (<a
href="https://redirect.github.com/pgjdbc/pgjdbc/issues/3897">#3897</a>)</li>
<li>fix badges for maven central and search paths. Sonatype has changed
the search paths <a
href="https://github.com/davecramer"><code>@​davecramer</code></a> (<a
href="https://redirect.github.com/pgjdbc/pgjdbc/issues/3901">#3901</a>)</li>
<li>fix: make all Calendar instances proleptic Gregorian (<a
href="https://redirect.github.com/pgjdbc/pgjdbc/issues/3837">#3837</a>)
<a
href="https://github.com/m-van-tilburg"><code>@​m-van-tilburg</code></a>
(<a
href="https://redirect.github.com/pgjdbc/pgjdbc/issues/3887">#3887</a>)</li>
<li>test: add CI tests with Java 26 <a
href="https://github.com/vlsi"><code>@​vlsi</code></a> (<a
href="https://redirect.github.com/pgjdbc/pgjdbc/issues/3893">#3893</a>)</li>
<li>perf: optimize PGInterval.getValue() by replacing String.format with
StringBuilder <a href="https://github.com/vlsi"><code>@​vlsi</code></a>
(<a
href="https://redirect.github.com/pgjdbc/pgjdbc/issues/3866">#3866</a>)</li>
<li>use ssl_is_used() to check for ssl connection <a
href="https://github.com/davecramer"><code>@​davecramer</code></a> (<a
href="https://redirect.github.com/pgjdbc/pgjdbc/issues/3867">#3867</a>)</li>
<li>Add PEMKeyManager to handle PEM based certs and keys. <a
href="https://github.com/harinath001"><code>@​harinath001</code></a> (<a
href="https://redirect.github.com/pgjdbc/pgjdbc/issues/3700">#3700</a>)</li>
<li>Comment and simplify the complex state machine logic in
QueryExecutorImpl <a
href="https://github.com/davecramer"><code>@​davecramer</code></a> (<a
href="https://redirect.github.com/pgjdbc/pgjdbc/issues/3850">#3850</a>)</li>
<li>Revert &quot;fix: Issue <a
href="https://redirect.github.com/pgjdbc/pgjdbc/issues/3784">#3784</a>
pgjdbc can't decode numeric arrays containing special numbers like
NaN&quot; <a
href="https://github.com/davecramer"><code>@​davecramer</code></a> (<a
href="https://redirect.github.com/pgjdbc/pgjdbc/issues/3851">#3851</a>)</li>
<li>fix: Issue <a
href="https://redirect.github.com/pgjdbc/pgjdbc/issues/3784">#3784</a>
pgjdbc can't decode numeric arrays containing special numbers like NaN
<a href="https://github.com/ShenFeng312"><code>@​ShenFeng312</code></a>
(<a
href="https://redirect.github.com/pgjdbc/pgjdbc/issues/3838">#3838</a>)</li>
<li>Small simplication of locking patterns in QueryExecutorBase <a
href="https://github.com/Sanne"><code>@​Sanne</code></a> (<a
href="https://redirect.github.com/pgjdbc/pgjdbc/issues/3849">#3849</a>)</li>
<li>doc: update property quoteReturningIdentifiers default value <a
href="https://github.com/sodekim"><code>@​sodekim</code></a> (<a
href="https://redirect.github.com/pgjdbc/pgjdbc/issues/3847">#3847</a>)</li>
<li>feat: default query timeout property <a
href="https://github.com/cfredri4"><code>@​cfredri4</code></a> (<a
href="https://redirect.github.com/pgjdbc/pgjdbc/issues/3705">#3705</a>)</li>
<li>create action to deploy docs to <a
href="https://pgjdbc.github.io/">https://pgjdbc.github.io/</a> <a
href="https://github.com/davecramer"><code>@​davecramer</code></a> (<a
href="https://redirect.github.com/pgjdbc/pgjdbc/issues/3819">#3819</a>)</li>
<li>fix homepage release note <a
href="https://github.com/davecramer"><code>@​davecramer</code></a> (<a
href="https://redirect.github.com/pgjdbc/pgjdbc/issues/3817">#3817</a>)</li>
</ul>
<h2>🐛 Bug Fixes</h2>
<ul>
<li>fix: close temporary lob descriptors that are used internally in
PreparedStatement#setBlob <a
href="https://github.com/vlsi"><code>@​vlsi</code></a> (<a
href="https://redirect.github.com/pgjdbc/pgjdbc/issues/3903">#3903</a>)</li>
<li>fix: avoid memory leaks in Java &lt;= 21 caused by
Thread.inheritedAccessControlContext <a
href="https://github.com/vlsi"><code>@​vlsi</code></a> (<a
href="https://redirect.github.com/pgjdbc/pgjdbc/issues/3886">#3886</a>)</li>
</ul>
<h2>📝 Documentation</h2>
<ul>
<li>doc: add the new PGP signing key to the official documentation <a
href="https://github.com/vlsi"><code>@​vlsi</code></a> (<a
href="https://redirect.github.com/pgjdbc/pgjdbc/issues/3813">#3813</a>)</li>
</ul>
<h2>🧰 Maintenance</h2>
<ul>
<li>chore: remove unused com.github.spotbugs Gradle plugin dependency <a
href="https://github.com/vlsi"><code>@​vlsi</code></a> (<a
href="https://redirect.github.com/pgjdbc/pgjdbc/issues/3868">#3868</a>)</li>
<li>chore: drop SpotBugs as we do not seem to use it <a
href="https://github.com/vlsi"><code>@​vlsi</code></a> (<a
href="https://redirect.github.com/pgjdbc/pgjdbc/issues/3834">#3834</a>)</li>
<li>chore: bump version to 42.7.9 after 42.7.8 release <a
href="https://github.com/vlsi"><code>@​vlsi</code></a> (<a
href="https://redirect.github.com/pgjdbc/pgjdbc/issues/3810">#3810</a>)</li>
</ul>
<h2>⬆️ Dependencies</h2>
<!-- raw HTML omitted -->
<ul>
<li>chore(deps): update actions/create-github-app-token digest to
29824e6 <a
href="https://github.com/renovate-bot"><code>@​renovate-bot</code></a>
(<a
href="https://redirect.github.com/pgjdbc/pgjdbc/issues/3898">#3898</a>)</li>
<li>chore(deps): update actions/setup-java digest to c1e3236 <a
href="https://github.com/renovate-bot"><code>@​renovate-bot</code></a>
(<a
href="https://redirect.github.com/pgjdbc/pgjdbc/issues/3899">#3899</a>)</li>
<li>chore(deps): update codecov/codecov-action digest to 671740a <a
href="https://github.com/renovate-bot"><code>@​renovate-bot</code></a>
(<a
href="https://redirect.github.com/pgjdbc/pgjdbc/issues/3900">#3900</a>)</li>
<li>fix(deps): update dependency org.junit:junit-bom to v5.14.1 -
autoclosed <a
href="https://github.com/renovate-bot"><code>@​renovate-bot</code></a>
(<a
href="https://redirect.github.com/pgjdbc/pgjdbc/issues/3884">#3884</a>)</li>
<li>fix(deps): update dependency org.apache.bcel:bcel to v6.11.0 <a
href="https://github.com/renovate-bot"><code>@​renovate-bot</code></a>
(<a
href="https://redirect.github.com/pgjdbc/pgjdbc/issues/3883">#3883</a>)</li>
<li>fix(deps): update dependency org.mockito:mockito-bom to v5.20.0 <a
href="https://github.com/renovate-bot"><code>@​renovate-bot</code></a>
(<a
href="https://redirect.github.com/pgjdbc/pgjdbc/issues/3885">#3885</a>)</li>
<li>fix(deps): update dependency net.bytebuddy:byte-buddy-parent to
v1.18.2 <a
href="https://github.com/renovate-bot"><code>@​renovate-bot</code></a>
(<a
href="https://redirect.github.com/pgjdbc/pgjdbc/issues/3882">#3882</a>)</li>
<li>chore(deps): update github/codeql-action digest to 497990d <a
href="https://github.com/renovate-bot"><code>@​renovate-bot</code></a>
(<a
href="https://redirect.github.com/pgjdbc/pgjdbc/issues/3881">#3881</a>)</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/pgjdbc/pgjdbc/blob/master/CHANGELOG.md">org.postgresql:postgresql's
changelog</a>.</em></p>
<blockquote>
<h2>[42.7.9] (2026-01-14)</h2>
<h3>Added</h3>
<ul>
<li>feat: query timeout property [PR <a
href="https://redirect.github.com/pgjdbc/pgjdbc/issues/3705">#3705</a>](<a
href="https://redirect.github.com/pgjdbc/pgjdbc/pull/3705">pgjdbc/pgjdbc#3705</a>)</li>
<li>feat: Add PEMKeyManager to handle PEM based certs and keys [PR <a
href="https://redirect.github.com/pgjdbc/pgjdbc/issues/3700">#3700</a>](<a
href="https://redirect.github.com/pgjdbc/pgjdbc/pull/3700">pgjdbc/pgjdbc#3700</a>)</li>
</ul>
<h3>Changed</h3>
<ul>
<li>perf: optimize PGInterval.getValue() by replacing String.format with
StringBuilder</li>
<li>doc: update property quoteReturningIdentifiers default value [PR <a
href="https://redirect.github.com/pgjdbc/pgjdbc/issues/3847">#3847</a>](<a
href="https://redirect.github.com/pgjdbc/pgjdbc/pull/3847">pgjdbc/pgjdbc#3847</a>)</li>
<li>security: Use a static method forName to load all user supplied
classes. Use the Class.forName 3 parameter method and do not initilize
it unless it is a subclass of the expected class</li>
</ul>
<h3>Fixed</h3>
<ul>
<li>fix: incorrect pg_stat_replication.reply_time calculation [PR <a
href="https://redirect.github.com/pgjdbc/pgjdbc/issues/3906">#3906</a>](<a
href="https://redirect.github.com/pgjdbc/pgjdbc/pull/3906">pgjdbc/pgjdbc#3906</a>)</li>
<li>fix: close temporary lob descriptors that are used internally in
PreparedStatement#setBlob</li>
<li>fix: PGXAConnection.prepare(Xid) should return XA_RDONLY if the
connection is read only [PR <a
href="https://redirect.github.com/pgjdbc/pgjdbc/issues/3897">#3897</a>](<a
href="https://redirect.github.com/pgjdbc/pgjdbc/pull/3897">pgjdbc/pgjdbc#3897</a>)</li>
<li>fix: make all Calendar instances proleptic Gregorian [PR <a
href="https://redirect.github.com/pgjdbc/pgjdbc/issues/3837">#3837</a>](<a
href="https://redirect.github.com/pgjdbc/pgjdbc/pull/3887">pgjdbc/pgjdbc#3887</a>)</li>
<li>fix: Simplify concurrency guards on QueryExecutorBase#transaction
and QueryExecutorBase#standardConformingStrings [PR <a
href="https://redirect.github.com/pgjdbc/pgjdbc/issues/3897">#3897</a>](<a
href="https://redirect.github.com/pgjdbc/pgjdbc/pull/3849">pgjdbc/pgjdbc#3849</a>)</li>
<li>fix: avoid memory leaks in Java &lt;= 21 caused by
Thread.inheritedAccessControlContext [PR <a
href="https://redirect.github.com/pgjdbc/pgjdbc/issues/3886">#3886</a>](<a
href="https://redirect.github.com/pgjdbc/pgjdbc/pull/3886">pgjdbc/pgjdbc#3886</a>)</li>
<li>fix: Issue <a
href="https://redirect.github.com/pgjdbc/pgjdbc/issues/3784">#3784</a>
pgjdbc can't decode numeric arrays containing special numbers like NaN
[PR <a
href="https://redirect.github.com/pgjdbc/pgjdbc/issues/3838">#3838</a>](<a
href="https://redirect.github.com/pgjdbc/pgjdbc/pull/3838">pgjdbc/pgjdbc#3838</a>)</li>
<li>fix: use ssl_is_used() to check for ssl connection [PR <a
href="https://redirect.github.com/pgjdbc/pgjdbc/issues/3867">#3867</a>](<a
href="https://redirect.github.com/pgjdbc/pgjdbc/pull/3867">pgjdbc/pgjdbc#3867</a>)</li>
<li>fix: the classloader is nullable [PR <a
href="https://redirect.github.com/pgjdbc/pgjdbc/issues/3907">#3907</a>](<a
href="https://redirect.github.com/pgjdbc/pgjdbc/pull/3907">pgjdbc/pgjdbc#3907</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/pgjdbc/pgjdbc/commit/79b784e3a63def9d12088471334399a53016d880"><code>79b784e</code></a>
Added changelogs for version 42.7.9 (<a
href="https://redirect.github.com/pgjdbc/pgjdbc/issues/3908">#3908</a>)</li>
<li><a
href="https://github.com/pgjdbc/pgjdbc/commit/1c00ffc02be1570027b6510cbcd760b916227800"><code>1c00ffc</code></a>
doc: add the new PGP signing key to the official documentation</li>
<li><a
href="https://github.com/pgjdbc/pgjdbc/commit/f774000c105ddb2971f50dc5cf51a2d20ee4c14a"><code>f774000</code></a>
chore(deps): update actions/create-github-app-token digest to
29824e6</li>
<li><a
href="https://github.com/pgjdbc/pgjdbc/commit/27daf3b48b8b7d266ba680f59345f3723e2786d2"><code>27daf3b</code></a>
chore(deps): update actions/setup-java digest to c1e3236</li>
<li><a
href="https://github.com/pgjdbc/pgjdbc/commit/6eb01ff6bcb7ba6d71c9363a29d7305911861946"><code>6eb01ff</code></a>
chore(deps): update codecov/codecov-action digest to 671740a</li>
<li><a
href="https://github.com/pgjdbc/pgjdbc/commit/dbf1e57747709b560da16fdcec0ba9e927393516"><code>dbf1e57</code></a>
the classloader is nullable, and remove a space (<a
href="https://redirect.github.com/pgjdbc/pgjdbc/issues/3907">#3907</a>)</li>
<li><a
href="https://github.com/pgjdbc/pgjdbc/commit/6a20574f4c896b3f02d7f36d21ab1f3da15c3936"><code>6a20574</code></a>
Merge commit from fork</li>
<li><a
href="https://github.com/pgjdbc/pgjdbc/commit/c07721af972eee4f10873b1a23b3811336454436"><code>c07721a</code></a>
fix: incorrect pg_stat_replication.reply_time calculation (<a
href="https://redirect.github.com/pgjdbc/pgjdbc/issues/3906">#3906</a>)</li>
<li><a
href="https://github.com/pgjdbc/pgjdbc/commit/83023f3c2ae98dc69df00a560dbbe386afafe3b1"><code>83023f3</code></a>
fix: close temporary lob descriptors that are used internally in
PreparedStat...</li>
<li><a
href="https://github.com/pgjdbc/pgjdbc/commit/62c9805ef8606f3d38273ac69f64b14e936a0bfa"><code>62c9805</code></a>
fix: issue <a
href="https://redirect.github.com/pgjdbc/pgjdbc/issues/3892">#3892</a>,
PGXAConnection.prepare(Xid) should return XA_RDONLY if the ...</li>
<li>Additional commits viewable in <a
href="https://github.com/pgjdbc/pgjdbc/compare/REL42.7.8...REL42.7.9">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=org.postgresql:postgresql&package-manager=gradle&previous-version=42.7.8&new-version=42.7.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-02-06 10:43:40 +00:00
dependabot[bot] 691fb80554 build(deps): bump io.swagger.core.v3:swagger-core-jakarta from 2.2.41 to 2.2.42 (#5549)
Bumps io.swagger.core.v3:swagger-core-jakarta from 2.2.41 to 2.2.42.


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=io.swagger.core.v3:swagger-core-jakarta&package-manager=gradle&previous-version=2.2.41&new-version=2.2.42)](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-02-06 10:43:23 +00:00
Ludy d4fda73e01 chore(vscode): replace deprecated Copilot extension with Copilot Chat (#5662)
# Description of Changes

- **What was changed**  
Updated `.vscode/extensions.json` to replace the deprecated
`GitHub.copilot` extension with `GitHub.copilot-chat`.

- **Why the change was made**  
The original GitHub Copilot extension is deprecated. Using the Copilot
Chat extension ensures continued support, compatibility with current
GitHub tooling, and access to the recommended Copilot experience in VS
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-02-06 10:42:41 +00:00
Balázs Szücs 0f5a0e694a feat:(pdfa-conversion) Implement Strict PDF/A Mode with Verification (#5663)
# Description of Changes


This PR introduces a new "Strict Mode" for the PDF to PDF/A conversion
tool. When enabled, the application will use VeraPDF to verify that the
resulting file is perfectly compliant with the selected PDF/A standard.
If validation fails, the system will return a descriptive error instead
of a non-compliant file.



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

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

Closes #(issue_number)
-->
<img width="371" height="993" alt="image"
src="https://github.com/user-attachments/assets/a22d50b0-ad7c-46b0-be79-b79c2bc80d92"
/>

---

## 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-02-06 10:38:39 +00:00
Anthony Stirling 00136f9e20 Saml fix (#5651)
# Description of Changes
When password login is disabled UI changes to have central style SSO
button

<img width="2057" height="1369" alt="image"
src="https://github.com/user-attachments/assets/8f65f778-0809-4c54-a9c4-acf3a67cfa63"
/>

Auto SSO login functionality

Massively increases auth debugging visibility: verbose console logging
in ErrorBoundary, AuthProvider, Landing, AuthCallback.

Improves OAuth/SAML testability: adds Keycloak docker-compose setups +
realm JSON exports + start/validate scripts for OAuth and SAML
environments.

Hardens license upload path handling: better logs + safer directory
traversal protection by normalizing absolute paths before startsWith
check.

UI polish for SSO-only login: new “single provider” centered layout +
updated button styles (pill buttons, variants, icon wrapper, arrow).


<!--
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-02-05 12:26:41 +00:00
stirlingbot[bot]andAnthony Stirling a844c7d09e 🤖 format everything with pre-commit by stirlingbot (#5642)
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-02-05 00:12:57 +00:00
Ludy 0128cad683 feat(i18n): add missing compare placeholder and stamp label translations (#5636)
# Description of Changes

This pull request updates the English (UK) translation file to add new
labels and placeholders for PDF comparison and stamping features.

Improvements to PDF comparison UI:

* Added a placeholder text `Select the edited PDF` for the edited PDF
selection field to improve user guidance.

Enhancements to PDF stamping options:

* Added new labels for stamp types including `Page Numbers`, `Draft
Watermark`, `Document Info`, `Legal Footer`, and clarified the format
for `European Date` as `European Date (DD/MM/YYYY)`.

---

## 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-02-05 00:06:04 +00:00
albanobattistellaandLudy fc5acc9d92 Updated Italian translation (#5637)
# 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
- [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.

---------

Co-authored-by: Ludy <Ludy87@users.noreply.github.com>
2026-02-05 00:05:38 +00:00
Anthony Stirling d89b96088b diagnostic script (#5646)
# 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-02-04 23:20:38 +00:00
Reece Browne 9cec2d14cb Bug/page editor additional fixes (#5660) 2026-02-04 18:44:49 +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
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
1613 changed files with 440971 additions and 315753 deletions
+40 -63
View File
@@ -1,75 +1,52 @@
# Node modules and build artifacts
node_modules
frontend/node_modules
frontend/dist
frontend/build
frontend/.vite
frontend/.tauri
frontend/src-tauri/target
# Gradle build artifacts
.gradle
build
bin
target
out
# Git
.git
# Version control
.git/
.gitignore
# IDE
.vscode
.idea
# Build outputs
build/
*/build/
**/build/
out/
target/
# Gradle caches (local, not what's in the container)
.gradle/
**/.gradle/
# Node / frontend
node_modules/
**/node_modules/
.npm/
.yarn/
# IDE and editor
.idea/
.vscode/
*.iml
*.iws
*.ipr
*.iws
# Logs
# Logs and temp files
*.log
logs
# Environment files
.env
.env.*
!.env.example
# OS files
*.tmp
*.pid
.DS_Store
Thumbs.db
# Java compiled files
*.class
*.jar
*.war
*.ear
# Test reports
test-results
coverage
# Docker
docker-compose.override.yml
# Docker itself
Dockerfile*
.dockerignore
# Temporary files
tmp
temp
*.tmp
*.swp
*~
# Runtime database and config files (locked by running app)
app/core/configs/**
stirling/**
stirling-pdf-DB*.mv.db
stirling-pdf-DB*.trace.db
# Documentation
*.md
!README.md
docs
# CI/CD
.github
# CI / CD configs (not needed in build context)
.github/
.circleci/
.gitlab-ci.yml
# Test reports
**/test-results/
**/jacoco/
# Local env
.env
.env.*
!.env.example
+1 -1
View File
@@ -14,7 +14,7 @@ indent_size = 4
max_line_length = 100
[*.py]
indent_size = 2
indent_size = 4
[*.gradle]
indent_size = 4
+5
View File
@@ -68,3 +68,8 @@ updates:
schedule:
interval: "weekly"
rebase-strategy: "auto"
- package-ecosystem: cargo
directory: /frontend/src-tauri/provisioner
schedule:
interval: daily
-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/**/.*'
+19 -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):
@@ -259,10 +259,27 @@ def check_for_differences(reference_file, file_list, branch, actor):
report.append(
f" - **_Extra keys in `{locale_dir}/{basename_current_file}`_**: `{missing_keys_str}` that are not present in **_`{basename_reference_file}`_**."
)
report.append("")
report.append(" Use the following command to remove them:")
report.append(
f" `python scripts/translations/translation_merger.py {locale_dir} remove-unused`"
)
report.append("")
if extra_keys_list:
report.append(
f" - **_Missing keys in `{locale_dir}/{basename_current_file}`_**: `{extra_keys_str}` that are not present in **_`{basename_reference_file}`_**."
)
report.append("")
report.append(" Use the following command to add them:")
report.append(
f" `python scripts/translations/translation_merger.py {locale_dir} add-missing`"
)
report.append("")
if missing_keys_list or extra_keys_list:
report.append(
" See: https://github.com/Stirling-Tools/Stirling-PDF/tree/main/scripts/translations#2-translation_mergerpy"
)
else:
report.append("2. **Test Status:** ✅ **_Passed_**")
+289 -338
View File
@@ -9,7 +9,7 @@
# CVE-2025-6176 mitigation: pin brotli to a specific commit
brotli @ git+https://github.com/google/brotli.git@028fb5a23661f123017c060daa546b55cf4bde29
# via
# -r .github/scripts/requirements_dev.in
# -r requirements_dev.in
# fonttools
cffi==2.0.0 \
--hash=sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb \
@@ -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
# via -r .github/scripts/requirements_dev.in
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 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
# via -r requirements_dev.in
pillow==12.1.1 \
--hash=sha256:02f84dfad02693676692746df05b89cf25597560db2857363a208e393429f5e9 \
--hash=sha256:0330d233c1a0ead844fc097a7d16c0abff4c12e856c0b325f231820fee1f39da \
--hash=sha256:03edcc34d688572014ff223c125a3f77fb08091e4607e7745002fc214070b35f \
--hash=sha256:097690ba1f2efdeb165a20469d59d8bb03c55fb6621eb2041a060ae8ea3e9642 \
--hash=sha256:178aa072084bd88ec759052feca8e56cbb14a60b39322b99a049e58090479713 \
--hash=sha256:18e5bddd742a44b7e6b1e773ab5db102bd7a94c32555ba656e76d319d19c3850 \
--hash=sha256:1a9b0ee305220b392e1124a764ee4265bd063e54a751a6b62eff69992f457fa9 \
--hash=sha256:1f1625b72740fdda5d77b4def688eb8fd6490975d06b909fd19f13f391e077e0 \
--hash=sha256:1f1be78ce9466a7ee64bfda57bdba0f7cc499d9794d518b854816c41bf0aa4e9 \
--hash=sha256:1f90cff8aa76835cba5769f0b3121a22bd4eb9e6884cfe338216e557a9a548b8 \
--hash=sha256:21329ec8c96c6e979cd0dfd29406c40c1d52521a90544463057d2aaa937d66a6 \
--hash=sha256:2815a87ab27848db0321fb78c7f0b2c8649dee134b7f2b80c6a45c6831d75ccd \
--hash=sha256:2c1fc0f2ca5f96a3c8407e41cca26a16e46b21060fe6d5b099d2cb01412222f5 \
--hash=sha256:2e0c664be47252947d870ac0d327fea7e63985a08794758aa8af5b6cb6ec0c9c \
--hash=sha256:339ffdcb7cbeaa08221cd401d517d4b1fe7a9ed5d400e4a8039719238620ca35 \
--hash=sha256:344cf1e3dab3be4b1fa08e449323d98a2a3f819ad20f4b22e77a0ede31f0faa1 \
--hash=sha256:36341d06738a9f66c8287cf8b876d24b18db9bd8740fa0672c74e259ad408cff \
--hash=sha256:365b10bb9417dd4498c0e3b128018c4a624dc11c7b97d8cc54effe3b096f4c38 \
--hash=sha256:3a5cbdcddad0af3da87cb16b60d23648bc3b51967eb07223e9fed77a82b457c4 \
--hash=sha256:417423db963cb4be8bac3fc1204fe61610f6abeed1580a7a2cbb2fbda20f12af \
--hash=sha256:42fc1f4677106188ad9a55562bbade416f8b55456f522430fadab3cef7cd4e60 \
--hash=sha256:44ce27545b6efcf0fdbdceb31c9a5bdea9333e664cda58a7e674bb74608b3986 \
--hash=sha256:472a8d7ded663e6162dafdf20015c486a7009483ca671cece7a9279b512fcb13 \
--hash=sha256:47b94983da0c642de92ced1702c5b6c292a84bd3a8e1d1702ff923f183594717 \
--hash=sha256:495c302af3aad1ca67420ddd5c7bd480c8867ad173528767d906428057a11f0e \
--hash=sha256:4ceb838d4bd9dab43e06c363cab2eebf63846d6a4aeaea283bbdfd8f1a8ed58b \
--hash=sha256:50480dcd74fa63b8e78235957d302d98d98d82ccbfac4c7e12108ba9ecbdba15 \
--hash=sha256:518a48c2aab7ce596d3bf79d0e275661b846e86e4d0e7dec34712c30fe07f02a \
--hash=sha256:559b38da23606e68681337ad74622c4dbba02254fc9cb4488a305dd5975c7eeb \
--hash=sha256:578510d88c6229d735855e1f278aa305270438d36a05031dfaae5067cc8eb04d \
--hash=sha256:597bd9c8419bc7c6af5604e55847789b69123bbe25d65cc6ad3012b4f3c98d8b \
--hash=sha256:5a8eb7ed8d4198bccbd07058416eeec51686b498e784eda166395a23eb99138e \
--hash=sha256:5c0dd1636633e7e6a0afe7bf6a51a14992b7f8e60de5789018ebbdfae55b040a \
--hash=sha256:5cb1785d97b0c3d1d1a16bc1d710c4a0049daefc4935f3a8f31f827f4d3d2e7f \
--hash=sha256:5d1f9575a12bed9e9eedd9a4972834b08c97a352bd17955ccdebfeca5913fa0a \
--hash=sha256:5d8c41325b382c07799a3682c1c258469ea2ff97103c53717b7893862d0c98ce \
--hash=sha256:5dae5f21afb91322f2ff791895ddd8889e5e947ff59f71b46041c8ce6db790bc \
--hash=sha256:600fd103672b925fe62ed08e0d874ea34d692474df6f4bf7ebe148b30f89f39f \
--hash=sha256:6408a7b064595afcab0a49393a413732a35788f2a5092fdc6266952ed67de586 \
--hash=sha256:652a2c9ccfb556235b2b501a3a7cf3742148cd22e04b5625c5fe057ea3e3191f \
--hash=sha256:665e1b916b043cef294bc54d47bf02d87e13f769bc4bc5fa225a24b3a6c5aca9 \
--hash=sha256:691ab2ac363b8217f7d31b3497108fb1f50faab2f75dfb03284ec2f217e87bf8 \
--hash=sha256:6c52f062424c523d6c4db85518774cc3d50f5539dd6eed32b8f6229b26f24d40 \
--hash=sha256:6c6db3b84c87d48d0088943bf33440e0c42370b99b1c2a7989216f7b42eede60 \
--hash=sha256:7311c0a0dcadb89b36b7025dfd8326ecfa36964e29913074d47382706e516a7c \
--hash=sha256:7aac39bcf8d4770d089588a2e1dd111cbaa42df5a94be3114222057d68336bd0 \
--hash=sha256:7b03048319bfc6170e93bd60728a1af51d3dd7704935feb228c4d4faab35d334 \
--hash=sha256:7e7976bf1910a8116b523b9f9f58bf410f3e8aa330cd9a2bb2953f9266ab49af \
--hash=sha256:8089c852a56c2966cf18835db62d9b34fef7ba74c726ad943928d494fa7f4735 \
--hash=sha256:86172b0831b82ce4f7877f280055892b31179e1576aa00d0df3bb1bbf8c3e524 \
--hash=sha256:89b54027a766529136a06cfebeecb3a04900397a3590fd252160b888479517bf \
--hash=sha256:89c7e895002bbe49cdc5426150377cbbc04767d7547ed145473f496dfa40408b \
--hash=sha256:8b7e5304e34942bf62e15184219a7b5ad4ff7f3bb5cca4d984f37df1a0e1aee2 \
--hash=sha256:8fd420ef0c52c88b5a035a0886f367748c72147b2b8f384c9d12656678dfdfa9 \
--hash=sha256:98edb152429ab62a1818039744d8fbb3ccab98a7c29fc3d5fcef158f3f1f68b7 \
--hash=sha256:99c1506ea77c11531d75e3a412832a13a71c7ebc8192ab9e4b2e355555920e3e \
--hash=sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4 \
--hash=sha256:9f51079765661884a486727f0729d29054242f74b46186026582b4e4769918e4 \
--hash=sha256:a003d7422449f6d1e3a34e3dd4110c22148336918ddbfc6a32581cd54b2e0b2b \
--hash=sha256:a0b1cd6232e2b618adcc54d9882e4e662a089d5768cd188f7c245b4c8c44a397 \
--hash=sha256:a285e3eb7a5a45a2ff504e31f4a8d1b12ef62e84e5411c6804a42197c1cf586c \
--hash=sha256:a37691702ed687799de29a518d63d4682d9016932db66d4e90c345831b02fb4e \
--hash=sha256:a550ae29b95c6dc13cf69e2c9dc5747f814c54eeb2e32d683e5e93af56caa029 \
--hash=sha256:ab174cd7d29a62dd139c44bf74b698039328f45cb03b4596c43473a46656b2f3 \
--hash=sha256:ab323b787d6e18b3d91a72fc99b1a2c28651e4358749842b8f8dfacd28ef2052 \
--hash=sha256:adebb5bee0f0af4909c30db0d890c773d1a92ffe83da908e2e9e720f8edf3984 \
--hash=sha256:aee2810642b2898bb187ced9b349e95d2a7272930796e022efaf12e99dccd293 \
--hash=sha256:af9a332e572978f0218686636610555ae3defd1633597be015ed50289a03c523 \
--hash=sha256:b574c51cf7d5d62e9be37ba446224b59a2da26dc4c1bb2ecbe936a4fb1a7cb7f \
--hash=sha256:b66e95d05ba806247aaa1561f080abc7975daf715c30780ff92a20e4ec546e1b \
--hash=sha256:b81b5e3511211631b3f672a595e3221252c90af017e399056d0faabb9538aa80 \
--hash=sha256:b957b71c6b2387610f556a7eb0828afbe40b4a98036fc0d2acfa5a44a0c2036f \
--hash=sha256:bb66b7cc26f50977108790e2456b7921e773f23db5630261102233eb355a3b79 \
--hash=sha256:c6008de247150668a705a6338156efb92334113421ceecf7438a12c9a12dab23 \
--hash=sha256:c7697918b5be27424e9ce568193efd13d925c4481dd364e43f5dff72d33e10f8 \
--hash=sha256:cb9bb857b2d057c6dfc72ac5f3b44836924ba15721882ef103cecb40d002d80e \
--hash=sha256:cc7d296b5ea4d29e6570dabeaed58d31c3fea35a633a69679fb03d7664f43fb3 \
--hash=sha256:d242e8ac078781f1de88bf823d70c1a9b3c7950a44cdf4b7c012e22ccbcd8e4e \
--hash=sha256:d2912fd8114fc5545aa3a4b5576512f64c55a03f3ebcca4c10194d593d43ea36 \
--hash=sha256:d470ab1178551dd17fdba0fef463359c41aaa613cdcd7ff8373f54be629f9f8f \
--hash=sha256:d4ce8e329c93845720cd2014659ca67eac35f6433fd3050393d85f3ecef0dad5 \
--hash=sha256:d6e4571eedf43af33d0fc233a382a76e849badbccdf1ac438841308652a08e1f \
--hash=sha256:e65498daf4b583091ccbb2556c7000abf0f3349fcd57ef7adc9a84a394ed29f6 \
--hash=sha256:e879bb6cd5c73848ef3b2b48b8af9ff08c5b71ecda8048b7dd22d8a33f60be32 \
--hash=sha256:e9e8064fb1cc019296958595f6db671fba95209e3ceb0c4734c9baf97de04b20 \
--hash=sha256:f7ed2c6543bad5a7d5530eb9e78c53132f93dfa44a28492db88b41cdab885202 \
--hash=sha256:f95c00d5d6700b2b890479664a06e754974848afaae5e21beb4d83c106923fd0 \
--hash=sha256:f975aa7ef9684ce7e2c18a3aa8f8e2106ce1e46b94ab713d156b2898811651d3 \
--hash=sha256:fbfa2a7c10cc2623f412753cddf391c7f971c52ca40a3f65dc5039b2939e8563 \
--hash=sha256:fc354a04072b765eccf2204f588a7a532c9511e8b9c7f900e1b64e3e33487090 \
--hash=sha256:fc44ef1f3de4f45b50ccf9136999d71abb99dca7706bc75d222ed350b9fd2289
# via
# -r .github/scripts/requirements_dev.in
# -r 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
# via -r .github/scripts/requirements_dev.in
pycparser==2.23 \
--hash=sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2 \
--hash=sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934
pre-commit==4.5.1 \
--hash=sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77 \
--hash=sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61
# via -r requirements_dev.in
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,18 +462,18 @@ tinyhtml5==2.0.0 \
--hash=sha256:086f998833da24c300c414d9fe81d9b368fd04cb9d2596a008421cbc705fcfcc \
--hash=sha256:13683277c5b176d070f82d099d977194b7a1e26815b016114f581a74bbfbf47e
# via weasyprint
unoserver==3.4 \
--hash=sha256:3dcf2204013def1d1ddd3671f38b11346bdf349fef9728277462666a8a634419 \
--hash=sha256:64c24d33d4f65d680a2d9f676518cb28e7fd6c1f9d9a745c33e4a4cb59afdfcd
# via -r .github/scripts/requirements_dev.in
virtualenv==20.35.4 \
--hash=sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c \
--hash=sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b
unoserver==3.6 \
--hash=sha256:25c360fa194396a89cb79b4edd2735f8e4f0fd8531e59db3952114585bd7df05 \
--hash=sha256:e446bcb3638c51880f002aaeecab1cf74dfa9df81035f027f7ff2e081b6d7015
# via -r requirements_dev.in
virtualenv==20.36.1 \
--hash=sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f \
--hash=sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba
# via pre-commit
weasyprint==66.0 \
--hash=sha256:82b0783b726fcd318e2c977dcdddca76515b30044bc7a830cc4fbe717582a6d0 \
--hash=sha256:da71dc87dc129ac9cffdc65e5477e90365ab9dbae45c744014ec1d06303dde40
# via -r .github/scripts/requirements_dev.in
weasyprint==68.0 \
--hash=sha256:447f40898b747cb44ac31a5d493d512e7441fd56e13f63744c099383bbf9cda9 \
--hash=sha256:c2cb40c71b50837c5971f00171c9e4078e8c9912dd7c217f3e90e068f11e8aa1
# via -r requirements_dev.in
webencodings==0.5.1 \
--hash=sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78 \
--hash=sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923
@@ -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
# via -r .github/scripts/requirements_dev.in
setuptools==80.9.0 \
--hash=sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922 \
--hash=sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c
# via -r .github/scripts/requirements_dev.in
pip==26.0 \
--hash=sha256:3ce220a0a17915972fbf1ab451baae1521c4539e778b28127efa79b974aff0fa \
--hash=sha256:98436feffb9e31bc9339cf369fd55d3331b1580b6a6f1173bacacddcf9c34754
# via -r requirements_dev.in
setuptools==80.10.2 \
--hash=sha256:8b0e9d10c784bf7d262c4e5ec5d4ec94127ce206e8738f29a437945fbc219b70 \
--hash=sha256:95b30ddfb717250edb492926c92b5221f7ef3fbcc2b07579bcd4a27da21d0173
# via -r 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
+9 -7
View File
@@ -35,7 +35,7 @@ jobs:
pr_ref: ${{ steps.resolve.outputs.ref }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1
with:
egress-policy: audit
@@ -111,12 +111,12 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1
with:
egress-policy: audit
- name: Checkout main repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
repository: ${{ needs.check-pr.outputs.pr_repository }}
ref: ${{ needs.check-pr.outputs.pr_ref }}
@@ -189,7 +189,7 @@ jobs:
echo "versionNumber=$VERSION" >> $GITHUB_OUTPUT
- name: Login to Docker Hub
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_API }}
@@ -231,6 +231,8 @@ jobs:
context: .
file: ./docker/embedded/Dockerfile
push: true
cache-from: type=gha,scope=stirling-pdf-latest
cache-to: type=gha,mode=max,scope=stirling-pdf-latest
tags: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-${{ steps.commit-hash.outputs.app_short }}
build-args: VERSION_TAG=v2-alpha
platforms: linux/amd64
@@ -357,12 +359,12 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup GitHub App Bot
if: github.actor != 'dependabot[bot]'
@@ -40,12 +40,12 @@ jobs:
enable_enterprise: ${{ steps.check-pro-flag.outputs.enable_enterprise }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1
with:
egress-policy: audit
- name: Checkout PR
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup GitHub App Bot
if: github.actor != 'dependabot[bot]'
@@ -128,12 +128,12 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1
with:
egress-policy: audit
- name: Checkout PR
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup GitHub App Bot
if: github.actor != 'dependabot[bot]'
@@ -145,17 +145,22 @@ jobs:
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- name: Checkout PR
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: refs/pull/${{ needs.check-comment.outputs.pr_number }}/merge
token: ${{ steps.setup-bot.outputs.token }}
- name: Set up JDK 21
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
- name: Set up JDK 25
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
java-version: "17"
java-version: "25"
distribution: "temurin"
- name: Setup Gradle
uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
with:
gradle-version: 9.3.1
- name: Run Gradle Command
run: |
if [ "${{ needs.check-comment.outputs.disable_security }}" == "true" ]; then
@@ -163,15 +168,18 @@ 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
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- name: Login to Docker Hub
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_API }}
@@ -182,6 +190,8 @@ jobs:
context: .
file: ./docker/embedded/Dockerfile
push: true
cache-from: type=gha,scope=stirling-pdf-latest
cache-to: type=gha,mode=max,scope=stirling-pdf-latest
tags: ${{ secrets.DOCKER_HUB_USERNAME }}/test:pr-${{ needs.check-comment.outputs.pr_number }}
build-args: VERSION_TAG=alpha
platforms: linux/amd64
@@ -362,12 +372,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1
with:
egress-policy: audit
- name: Check out the repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup GitHub App Bot
id: setup-bot
+2 -2
View File
@@ -21,12 +21,12 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1
with:
egress-policy: audit
- name: Checkout PR
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup GitHub App Bot
if: github.actor != 'dependabot[bot]'
+83
View File
@@ -0,0 +1,83 @@
name: AI Engine CI
on:
push:
branches: [main]
pull_request:
jobs:
engine:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
defaults:
run:
working-directory: engine
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v4
with:
enable-cache: true
- name: Install dependencies
run: make install
- name: Run fixers
# Ignore errors here because we're going to add comments for them in the following steps before actually failing
run: make fix || true
- name: Check for fixer changes
id: fixer_changes
run: |
if git diff --quiet; then
echo "changed=false" >> "$GITHUB_OUTPUT"
else
echo "changed=true" >> "$GITHUB_OUTPUT"
fi
- name: Post fixer suggestions
if: steps.fixer_changes.outputs.changed == 'true' && github.event_name == 'pull_request'
uses: reviewdog/action-suggester@v1
continue-on-error: true
with:
tool_name: engine-make-fix
github_token: ${{ secrets.GITHUB_TOKEN }}
filter_mode: file
fail_level: any
level: info
- name: Comment on fixer suggestions
if: steps.fixer_changes.outputs.changed == 'true' && github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: "The Python code in your PR has formatting/linting issues. Consider running `make fix` locally or setting up your editor's Ruff integration to auto-format and lint your files as you go, or commit the suggested changes on this PR.",
});
- name: Verify fixer changes are committed
if: steps.fixer_changes.outputs.changed == 'true'
run: |
if ! git diff --exit-code; then
echo "Fixes are out of date."
echo "Apply the reviewdog suggestions or run 'make fix' from engine/ and commit the updated files."
git --no-pager diff --stat
exit 1
fi
- name: Run linting
run: make lint
- name: Run type checking
run: make typecheck
- name: Run tests
run: make test
+3 -3
View File
@@ -19,11 +19,11 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1
with:
egress-policy: audit
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
@@ -87,7 +87,7 @@ jobs:
- name: AI PR Title Analysis
if: steps.actor.outputs.is_repo_dev == 'true'
id: ai-title-analysis
uses: actions/ai-inference@334892bb203895caaed82ec52d23c1ed9385151e # v2.0.4
uses: actions/ai-inference@e09e65981758de8b2fdab13c2bfb7c7d5493b0b6 # v2.0.7
with:
model: openai/gpt-4o
system-prompt-file: ".github/config/system-prompt.txt"
+3 -3
View File
@@ -16,11 +16,11 @@ jobs:
pull-requests: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1
with:
egress-policy: audit
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup GitHub App Bot
id: setup-bot
@@ -29,7 +29,7 @@ jobs:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- uses: srvaroa/labeler@0a20eccb8c94a1ee0bed5f16859aece1c45c3e55 # v1.13.0
- uses: srvaroa/labeler@bf262763a8a8e191f5847873aecc0f29df84f957 # v1.14.0
with:
config_path: .github/labeler-config-srvaroa.yml
use_local_config: false
+170 -43
View File
@@ -32,14 +32,14 @@ jobs:
frontend: ${{ steps.changes.outputs.frontend }}
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Check for file changes
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
id: changes
with:
filters: .github/config/.files.yaml
@@ -52,25 +52,43 @@ jobs:
strategy:
fail-fast: false
matrix:
jdk-version: [17, 21]
jdk-version: [21, 25]
spring-security: [true, false]
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up JDK ${{ matrix.jdk-version }}
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
java-version: ${{ matrix.jdk-version }}
distribution: "temurin"
- name: Cache Gradle dependency artifacts
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: |
~/.gradle/wrapper
~/.gradle/caches/modules-2/files-2.1
~/.gradle/caches/modules-2/metadata-2.*
key: gradle-deps-${{ runner.os }}-jdk-${{ matrix.jdk-version }}-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties', '**/*.gradle', '**/*.gradle.kts', 'settings.gradle', 'settings.gradle.kts', 'gradle/libs.versions.toml') }}
- name: Setup Gradle
uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
with:
gradle-version: 9.3.1
cache-disabled: true
- 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
@@ -93,7 +111,7 @@ jobs:
- name: Upload Test Reports
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: test-reports-jdk-${{ matrix.jdk-version }}-spring-security-${{ matrix.spring-security }}
path: |
@@ -122,26 +140,44 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up JDK 21
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
- name: Set up JDK 25
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
java-version: "21"
java-version: "25"
distribution: "temurin"
- name: Cache Gradle dependency artifacts
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: |
~/.gradle/wrapper
~/.gradle/caches/modules-2/files-2.1
~/.gradle/caches/modules-2/metadata-2.*
key: gradle-deps-${{ runner.os }}-jdk-25-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties', '**/*.gradle', '**/*.gradle.kts', 'settings.gradle', 'settings.gradle.kts', 'gradle/libs.versions.toml') }}
- name: Setup Gradle
uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
with:
gradle-version: 9.3.1
cache-disabled: true
- 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
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: openapi-docs
path: ./SwaggerDoc.json
@@ -152,13 +188,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Node.js
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: "22"
cache: "npm"
@@ -166,7 +202,7 @@ jobs:
- name: Install frontend dependencies
run: cd frontend && npm ci
- name: Type-check frontend
run: cd frontend && npm run prebuild && npm run typecheck:all
run: cd frontend && npm run prep && npm run typecheck:all
- name: Lint frontend
run: cd frontend && npm run lint
- name: Build frontend
@@ -174,7 +210,7 @@ jobs:
- name: Run frontend tests
run: cd frontend && npm run test -- --run
- name: Upload frontend build artifacts
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: frontend-build
path: frontend/dist/
@@ -186,25 +222,49 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up JDK 21
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
- name: Set up JDK 25
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
java-version: "21"
java-version: "25"
distribution: "temurin"
- name: Cache Gradle dependency artifacts
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: |
~/.gradle/wrapper
~/.gradle/caches/modules-2/files-2.1
~/.gradle/caches/modules-2/metadata-2.*
key: gradle-deps-${{ runner.os }}-jdk-25-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties', '**/*.gradle', '**/*.gradle.kts', 'settings.gradle', 'settings.gradle.kts', 'gradle/libs.versions.toml') }}
- name: Setup Gradle
uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
with:
gradle-version: 9.3.1
cache-disabled: true
- name: check the licenses for compatibility
run: ./gradlew clean checkLicense
# NOTE: --no-parallel is intentional here. Running the checkLicense task in parallel with other
# Gradle tasks has been observed to cause intermittent failures with the dependency license
# checking plugin on this Gradle version. Disabling parallel execution trades some build speed
# for more reliable, deterministic license checks. If upgrading Gradle or the plugin, consider
# re-evaluating whether this flag is still required before removing it.
run: ./gradlew checkLicense --no-parallel
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()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: dependencies-without-allowed-license.json
path: build/reports/dependency-license/dependencies-without-allowed-license.json
@@ -228,32 +288,55 @@ jobs:
# )
runs-on: ubuntu-latest
permissions:
actions: write
contents: read
checks: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1
with:
egress-policy: audit
- name: Checkout Repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up JDK 21
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
- name: Set up JDK 25
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
java-version: "21"
java-version: "25"
distribution: "temurin"
- name: Cache Gradle dependency artifacts
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: |
~/.gradle/wrapper
~/.gradle/caches/modules-2/files-2.1
~/.gradle/caches/modules-2/metadata-2.*
key: gradle-deps-${{ runner.os }}-jdk-25-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties', '**/*.gradle', '**/*.gradle.kts', 'settings.gradle', 'settings.gradle.kts', 'gradle/libs.versions.toml') }}
- name: Setup Gradle
uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
with:
gradle-version: 9.3.1
cache-disabled: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
# Expose ACTIONS_RUNTIME_TOKEN / ACTIONS_RESULTS_URL for docker buildx type=gha cache backend.
- name: Expose GitHub runtime for Buildx cache
uses: crazy-max/ghaction-github-runtime@04d248b84655b509d8c44dc1d6f990c879747487 # v4.0.0
- name: Install Docker Compose
run: |
sudo curl -SL "https://github.com/docker/compose/releases/download/v2.39.4/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
- name: Set up Python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.12"
cache: "pip" # caching pip dependencies
@@ -262,6 +345,7 @@ jobs:
- name: Pip requirements
run: |
pip install --require-hashes -r ./testing/cucumber/requirements.txt
pip install behave-html-formatter
- name: Run Docker Compose Tests
run: |
@@ -269,6 +353,28 @@ 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 }}
- name: Upload Cucumber Report
if: always()
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: cucumber-report
path: testing/cucumber/report.html
retention-days: 7
if-no-files-found: warn
- name: Cucumber Test Report
if: always()
uses: dorny/test-reporter@b082adf0eced0765477756c2a610396589b8c637 # v2.5.0
with:
name: Cucumber Tests
path: testing/cucumber/junit/*.xml
reporter: java-junit
fail-on-error: false
test-build-docker-images:
if: github.event_name == 'pull_request' && needs.files-changed.outputs.project == 'true'
@@ -280,18 +386,21 @@ jobs:
include:
- docker-rev: docker/embedded/Dockerfile
artifact-suffix: Dockerfile
cache-scope: stirling-pdf-latest
- docker-rev: docker/embedded/Dockerfile.ultra-lite
artifact-suffix: Dockerfile.ultra-lite
cache-scope: stirling-pdf-ultra-lite
- docker-rev: docker/embedded/Dockerfile.fat
artifact-suffix: Dockerfile.fat
cache-scope: stirling-pdf-fat
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1
with:
egress-policy: audit
- name: Checkout Repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Free disk space on runner
run: |
@@ -300,20 +409,38 @@ jobs:
docker system prune -af || true
echo "Disk space after cleanup:" && df -h
- name: Set up JDK 21
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
- name: Set up JDK 25
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
java-version: "21"
java-version: "25"
distribution: "temurin"
- name: Cache Gradle dependency artifacts
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: |
~/.gradle/wrapper
~/.gradle/caches/modules-2/files-2.1
~/.gradle/caches/modules-2/metadata-2.*
key: gradle-deps-${{ runner.os }}-jdk-25-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties', '**/*.gradle', '**/*.gradle.kts', 'settings.gradle', 'settings.gradle.kts', 'gradle/libs.versions.toml') }}
- name: Setup Gradle
uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
with:
gradle-version: 9.3.1
cache-disabled: true
- 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
- name: Set up QEMU
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
- name: Set up Docker Buildx
id: buildx
@@ -326,15 +453,15 @@ jobs:
context: .
file: ./${{ matrix.docker-rev }}
push: false
cache-from: type=gha,scope=${{ matrix.artifact-suffix }}
cache-to: type=gha,mode=max,scope=${{ matrix.artifact-suffix }}
cache-from: type=gha,scope=${{ matrix.cache-scope }}
cache-to: type=gha,mode=max,scope=${{ matrix.cache-scope }}
platforms: linux/amd64,linux/arm64/v8
provenance: true
sbom: true
- name: Upload Reports
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: reports-docker-${{ matrix.artifact-suffix }}
path: |
+3 -3
View File
@@ -27,12 +27,12 @@ jobs:
pull-requests: write # Allow writing to pull requests
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1
with:
egress-policy: audit
- name: Checkout main branch first
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup GitHub App Bot
id: setup-bot
@@ -196,7 +196,7 @@ jobs:
core.exportVariable("REFERENCE_FILE", referenceFilePath);
- name: Set up Python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.12"
+2 -2
View File
@@ -17,12 +17,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1
with:
egress-policy: audit
- name: "Checkout Repository"
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: "Dependency Review"
uses: actions/dependency-review-action@3c4e3dcb1aa7874d2c16be7d79418e9b7efd6261 # v4.8.2
with:
+7 -3
View File
@@ -18,12 +18,12 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1
with:
egress-policy: audit
- name: Checkout code
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
@@ -85,7 +85,7 @@ jobs:
fi
- name: Login to Docker Hub
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_API }}
@@ -97,6 +97,8 @@ jobs:
context: .
file: ./docker/frontend/Dockerfile
push: true
cache-from: type=gha,scope=stirling-v2-frontend
cache-to: type=gha,mode=max,scope=stirling-v2-frontend
tags: |
${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-frontend-${{ steps.commit-hashes.outputs.frontend_short }}
${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-frontend-latest
@@ -110,6 +112,8 @@ jobs:
context: .
file: ./docker/backend/Dockerfile
push: true
cache-from: type=gha,scope=stirling-v2-backend
cache-to: type=gha,mode=max,scope=stirling-v2-backend
tags: |
${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-backend-${{ steps.commit-hashes.outputs.backend_short }}
${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-backend-latest
@@ -25,15 +25,15 @@ jobs:
licenses-backend: ${{ steps.changes.outputs.licenses-backend }}
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Check for file changes
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
id: changes
with:
filters: .github/config/.files.yaml
@@ -49,12 +49,12 @@ jobs:
repository-projects: write # Required for enabling automerge
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1
with:
egress-policy: audit
- name: Checkout PR head (default)
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.event.pull_request.base.sha }}
path: base
@@ -77,7 +77,7 @@ jobs:
persist-credentials: false
- name: Set up Node.js
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: "22"
cache: "npm"
@@ -273,7 +273,7 @@ jobs:
- name: Create Pull Request (Push only)
id: cpr
if: github.event_name == 'push' && env.CHANGES_DETECTED == 'true'
uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with:
token: ${{ steps.setup-bot.outputs.token }}
commit-message: "Update Frontend 3rd Party Licenses"
@@ -312,12 +312,12 @@ jobs:
repository-projects: write # Required for enabling automerge
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
persist-credentials: false
@@ -330,17 +330,30 @@ jobs:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- name: Set up JDK 21
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
- name: Set up JDK 25
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
java-version: "21"
java-version: "25"
distribution: "temurin"
- name: Setup Gradle
uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
with:
gradle-version: 9.3.1
- name: Check licenses and generate report
id: license-check
run: |
./gradlew clean checkLicense generateLicenseReport || echo "LICENSE_CHECK_FAILED=true" >> $GITHUB_ENV
# NOTE: --no-parallel is intentional here. Running the license-checking tasks in parallel has
# previously caused intermittent concurrency issues in CI (e.g. flaky failures in the license
# plugin/Gradle when multiple projects are evaluated concurrently). Disabling parallelism trades
# some build speed for more reliable license reports. If the underlying issues are resolved in
# future Gradle or plugin versions, this flag can be reconsidered.
./gradlew checkLicense generateLicenseReport --no-parallel || 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
@@ -356,7 +369,7 @@ jobs:
- name: Upload artifact on license issues
if: env.LICENSE_WARNINGS_EXIST == 'true'
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: backend-dependencies-without-allowed-license.json
path: build/reports/dependency-license/dependencies-without-allowed-license.json
@@ -482,7 +495,7 @@ jobs:
- name: Create Pull Request (push only)
if: github.event_name == 'push' && env.CHANGES_DETECTED == 'true'
id: cpr
uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with:
token: ${{ steps.setup-bot.outputs.token }}
commit-message: "Update Backend 3rd Party Licenses"
+2 -2
View File
@@ -15,12 +15,12 @@ jobs:
issues: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1
with:
egress-policy: audit
- name: Check out the repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Run Labeler
uses: crazy-max/ghaction-github-labeler@24d110aa46a59976b8a7f35518cb7f14f434c916 # v5.3.0
+64 -38
View File
@@ -21,8 +21,6 @@ on:
- windows
- macos
- linux
push:
branches: [main, V2-master]
release:
types: [created]
@@ -38,21 +36,32 @@ jobs:
version: ${{ steps.versionNumber.outputs.versionNumber }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1
with:
egress-policy: audit
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up JDK 21
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
- name: Set up JDK 25
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
java-version: "21"
java-version: "25"
distribution: "temurin"
- uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
- name: Cache Gradle dependencies
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
gradle-version: 8.14
path: |
~/.gradle/caches
~/.gradle/wrapper
key: gradle-${{ runner.os }}-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties') }}
restore-keys: |
gradle-${{ runner.os }}-
- name: Setup Gradle
uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
with:
gradle-version: 9.3.1
- name: Get version number
id: versionNumber
@@ -62,6 +71,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
@@ -106,33 +119,37 @@ jobs:
file_suffix: "-server"
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1
with:
egress-policy: audit
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up JDK 21
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
- name: Set up JDK 25
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
java-version: "21"
java-version: "25"
distribution: "temurin"
- uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
- name: Setup Gradle
uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
with:
gradle-version: 8.14
gradle-version: 9.3.1
- name: Setup Node.js
if: matrix.variant.build_frontend == true
uses: actions/setup-node@v4
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.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
@@ -145,7 +162,7 @@ jobs:
cp app/core/build/libs/stirling-pdf-${{ needs.determine-matrix.outputs.version }}.jar ./jar-dist/Stirling-PDF${{ matrix.variant.file_suffix }}.jar
- name: Upload JAR artifacts
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: jar${{ matrix.variant.file_suffix }}
path: ./jar-dist/*.jar
@@ -162,12 +179,12 @@ jobs:
WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install dependencies (ubuntu only)
if: matrix.platform == 'ubuntu-22.04'
@@ -176,31 +193,36 @@ 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@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.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' || '' }}
- name: Set up JDK 21
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
- name: Set up JDK 25
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
java-version: "21"
java-version: "25"
distribution: "temurin"
- name: Setup Gradle
uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
with:
gradle-version: 9.3.1
- 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 +286,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 +299,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@1d820463733701cf1484c7eb5d7d24a15ca2c454 # v1.2.1
env:
SM_API_KEY: ${{ secrets.SM_API_KEY }}
SM_CLIENT_CERT_FILE_B64: ${{ secrets.SM_CLIENT_CERT_FILE_B64 }}
@@ -382,7 +407,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 }}
@@ -394,8 +419,9 @@ jobs:
APPIMAGETOOL_SIGN_PASSPHRASE: ${{ secrets.APPIMAGETOOL_SIGN_PASSPHRASE }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY: ${{ secrets.VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY }}
VITE_SAAS_SERVER_URL: ${{ secrets.VITE_SAAS_SERVER_URL }}
VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY: ${{ secrets.VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY || 'sb_publishable_UHz2SVRF5mvdrPHWkRteyA_yNlZTkYb' }}
VITE_SAAS_SERVER_URL: ${{ secrets.VITE_SAAS_SERVER_URL || 'https://app.stirlingpdf.com' }}
VITE_SAAS_BACKEND_API_URL: ${{ secrets.VITE_SAAS_BACKEND_API_URL || 'https://api.stirlingpdf.com' }}
# Only enable Windows signing in Tauri when on release or V2-master
SIGN: ${{ (github.event_name == 'release' || github.ref == 'refs/heads/V2-master') && (env.SM_API_KEY == '' && env.WINDOWS_CERTIFICATE != '') && '1' || '0' }}
CI: true
@@ -516,7 +542,7 @@ jobs:
fi
- name: Upload build artifacts
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: Stirling-PDF-${{ matrix.name }}
path: ./dist/*
@@ -530,30 +556,30 @@ jobs:
contents: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1
with:
egress-policy: audit
- name: Download all Tauri artifacts
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: Stirling-PDF-*
path: ./artifacts/tauri
- name: Download JAR artifact (default)
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: jar
path: ./artifacts/jars
- name: Download JAR artifact (with login)
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
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@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: jar-server
path: ./artifacts/jars
@@ -562,7 +588,7 @@ jobs:
run: ls -R ./artifacts
- name: Upload binaries to Release
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1
with:
tag_name: v${{ needs.determine-matrix.outputs.version }}
generate_release_notes: true
+26 -9
View File
@@ -21,14 +21,15 @@ jobs:
pull-requests: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
persist-credentials: false
- name: Setup GitHub App Bot
id: setup-bot
@@ -38,7 +39,7 @@ jobs:
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- name: Set up Python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: 3.12
cache: "pip" # caching pip dependencies
@@ -48,17 +49,33 @@ jobs:
run: |
pip install --require-hashes --only-binary=:all: -r ./.github/scripts/requirements_pre_commit.txt
- run: pre-commit run --all-files -c .pre-commit-config.yaml
- name: Run Pre-Commit
run: |
pre-commit run ruff --all-files -c .pre-commit-config.yaml
pre-commit run ruff-format --all-files -c .pre-commit-config.yaml
pre-commit run codespell --all-files -c .pre-commit-config.yaml
pre-commit run gitleaks --all-files -c .pre-commit-config.yaml
pre-commit run end-of-file-fixer --all-files -c .pre-commit-config.yaml
pre-commit run trailing-whitespace --all-files -c .pre-commit-config.yaml
continue-on-error: true
- name: Set up JDK 21
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
- name: Set up JDK 25
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
java-version: 21
java-version: "25"
distribution: "temurin"
- name: Setup Gradle
uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
with:
gradle-version: 9.3.1
- 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: |
@@ -67,7 +84,7 @@ jobs:
- name: Create Pull Request
if: env.CHANGES_DETECTED == 'true'
uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with:
token: ${{ steps.setup-bot.outputs.token }}
commit-message: ":file_folder: pre-commit"
+41 -20
View File
@@ -33,21 +33,32 @@ jobs:
id-token: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1
with:
egress-policy: audit
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up JDK 21
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
- name: Set up JDK 25
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
java-version: "21"
java-version: "25"
distribution: "temurin"
- uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
- name: Cache Gradle dependencies
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
gradle-version: 8.14
path: |
~/.gradle/caches
~/.gradle/wrapper
key: gradle-${{ runner.os }}-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties') }}
restore-keys: |
gradle-${{ runner.os }}-
- name: Setup Gradle
uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
with:
gradle-version: 9.3.1
- name: Set up Docker Buildx
id: buildx
@@ -56,28 +67,38 @@ 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
uses: sigstore/cosign-installer@ba7bc0a3fef59531c69a25acd34668d6d3fe6f22 # v4.1.0
with:
cosign-release: "v2.4.1"
- name: Install cosign
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/V2-master'
uses: sigstore/cosign-installer@ba7bc0a3fef59531c69a25acd34668d6d3fe6f22 # v4.1.0
with:
cosign-release: "v2.4.1"
- name: Login to Docker Hub
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_API }}
- name: Login to GitHub Container Registry
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
- name: Set up QEMU
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
- name: Convert repository owner to lowercase
id: repoowner
@@ -85,7 +106,7 @@ jobs:
- name: Generate tags for latest
id: meta
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
with:
images: |
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
@@ -105,8 +126,8 @@ jobs:
context: .
file: ./docker/embedded/Dockerfile
push: true
cache-from: type=gha
cache-to: type=gha,mode=max
cache-from: type=gha,scope=stirling-pdf-latest
cache-to: type=gha,mode=max,scope=stirling-pdf-latest
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
@@ -130,7 +151,7 @@ jobs:
- name: Generate tags for latest-fat
id: meta-fat
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
if: github.ref != 'refs/heads/main' && github.ref != 'refs/heads/testMain'
with:
images: |
@@ -151,8 +172,8 @@ jobs:
context: .
file: ./docker/embedded/Dockerfile.fat
push: true
cache-from: type=gha
cache-to: type=gha,mode=max
cache-from: type=gha,scope=stirling-pdf-fat
cache-to: type=gha,mode=max,scope=stirling-pdf-fat
tags: ${{ steps.meta-fat.outputs.tags }}
labels: ${{ steps.meta-fat.outputs.labels }}
build-args: VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
@@ -174,7 +195,7 @@ jobs:
- name: Generate tags for ultra-lite
id: meta-lite
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
if: github.ref != 'refs/heads/main' && github.ref != 'refs/heads/testMain'
with:
images: |
@@ -195,8 +216,8 @@ jobs:
context: .
file: ./docker/embedded/Dockerfile.ultra-lite
push: true
cache-from: type=gha
cache-to: type=gha,mode=max
cache-from: type=gha,scope=stirling-pdf-ultra-lite
cache-to: type=gha,mode=max,scope=stirling-pdf-ultra-lite
tags: ${{ steps.meta-lite.outputs.tags }}
labels: ${{ steps.meta-lite.outputs.labels }}
build-args: VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
+4 -4
View File
@@ -35,12 +35,12 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1
with:
egress-policy: audit
- name: "Checkout code"
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
@@ -67,7 +67,7 @@ jobs:
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
# format to the repository Actions tab.
- name: "Upload artifact"
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: SARIF file
path: results.sarif
@@ -75,6 +75,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v3.29.5
uses: github/codeql-action/upload-sarif@0d579ffd059c29b07949a3cce3983f0780820c98 # v3.29.5
with:
sarif_file: results.sarif
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
pull-requests: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1
with:
egress-policy: audit
+16 -6
View File
@@ -27,19 +27,22 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1
with:
egress-policy: audit
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up JDK 21
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
- name: Set up JDK 25
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
java-version: "21"
java-version: "25"
distribution: "temurin"
- uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
- name: Setup Gradle
uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
with:
gradle-version: 9.3.1
- 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: |
+14 -5
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"
@@ -32,11 +35,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1
with:
egress-policy: audit
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Setup GitHub App Bot
id: setup-bot
@@ -46,18 +51,22 @@ jobs:
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- name: Set up Python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.12"
cache: "pip" # caching pip dependencies
- name: Install Python dependencies
run: pip install --require-hashes -r ./.github/scripts/requirements_sync_readme.txt
run: pip install --require-hashes -r ./.github/scripts/requirements_sync_readme.txt -r ./.github/scripts/requirements_pre_commit.txt
- name: Sync translation TOML files
run: |
python .github/scripts/check_language_toml.py --reference-file "frontend/public/locales/en-GB/translation.toml" --branch main
- name: pre-commit run
run: |
pre-commit run toml-sort-fix --all-files
- name: Commit translation files
run: |
git add frontend/public/locales/*/translation.toml
@@ -74,7 +83,7 @@ jobs:
- name: Create Pull Request
if: always()
uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with:
token: ${{ steps.setup-bot.outputs.token }}
commit-message: Update files
+167 -35
View File
@@ -14,17 +14,20 @@ on:
- macos
- linux
pull_request:
branches: [main, V2-tauri-windows]
branches: [main]
types: [opened, reopened, synchronize, ready_for_review]
paths:
- "frontend/src-tauri/**"
- "frontend/src/desktop/**"
- "frontend/tsconfig.desktop.json"
- "frontend/package.json"
- "frontend/package-lock.json"
- "frontend/vite.config.ts"
- ".github/workflows/tauri-build.yml"
push:
branches: [main]
permissions:
contents: read
pull-requests: write
jobs:
determine-matrix:
@@ -33,29 +36,49 @@ jobs:
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1
with:
egress-policy: audit
- name: Determine build matrix
id: set-matrix
env:
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
run: |
WINDOWS='{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64"}'
MACOS_ARM='{"platform":"macos-15","args":"--target aarch64-apple-darwin","name":"macos-aarch64"}'
MACOS_INTEL='{"platform":"macos-15-intel","args":"--target x86_64-apple-darwin","name":"macos-x86_64"}'
LINUX='{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64"}'
# Resolve requested platform (non-dispatch events always build all)
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
case "${{ github.event.inputs.platform }}" in
"windows")
echo 'matrix={"include":[{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64"}]}' >> $GITHUB_OUTPUT
;;
"macos")
echo 'matrix={"include":[{"platform":"macos-15","args":"--target aarch64-apple-darwin","name":"macos-aarch64"},{"platform":"macos-15-intel","args":"--target x86_64-apple-darwin","name":"macos-x86_64"}]}' >> $GITHUB_OUTPUT
;;
"linux")
echo 'matrix={"include":[{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64"}]}' >> $GITHUB_OUTPUT
;;
*)
echo 'matrix={"include":[{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64"},{"platform":"macos-15","args":"--target aarch64-apple-darwin","name":"macos-aarch64"},{"platform":"macos-15-intel","args":"--target x86_64-apple-darwin","name":"macos-x86_64"},{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64"}]}' >> $GITHUB_OUTPUT
;;
esac
PLATFORM="${{ github.event.inputs.platform }}"
else
# For PR/push events, build all platforms
echo 'matrix={"include":[{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64"},{"platform":"macos-15","args":"--target aarch64-apple-darwin","name":"macos-aarch64"},{"platform":"macos-15-intel","args":"--target x86_64-apple-darwin","name":"macos-x86_64"},{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64"}]}' >> $GITHUB_OUTPUT
PLATFORM="all"
fi
# Build candidate list
case "$PLATFORM" in
windows) ENTRIES=("$WINDOWS") ;;
macos) ENTRIES=("$MACOS_ARM" "$MACOS_INTEL") ;;
linux) ENTRIES=("$LINUX") ;;
*) ENTRIES=("$WINDOWS" "$MACOS_ARM" "$MACOS_INTEL" "$LINUX") ;;
esac
# Drop macOS entries when Apple certificate secret is unavailable
if [ -z "$APPLE_CERTIFICATE" ]; then
echo "⚠️ APPLE_CERTIFICATE secret not available - skipping macOS builds"
FILTERED=()
for entry in "${ENTRIES[@]}"; do
[[ "$entry" != *'"macos'* ]] && FILTERED+=("$entry")
done
ENTRIES=("${FILTERED[@]}")
fi
JOINED=$(IFS=','; echo "${ENTRIES[*]}")
echo "matrix={\"include\":[$JOINED]}" >> $GITHUB_OUTPUT
build:
needs: determine-matrix
strategy:
@@ -65,14 +88,15 @@ jobs:
env:
SM_API_KEY: ${{ secrets.SM_API_KEY }}
WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install dependencies (ubuntu only)
if: matrix.platform == 'ubuntu-22.04'
@@ -81,32 +105,37 @@ 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@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.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' || '' }}
- name: Set up JDK 21
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
- name: Set up JDK 25
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
java-version: "21"
java-version: "25"
distribution: "temurin"
- name: Setup Gradle
uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
with:
gradle-version: 9.3.1
- 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 +199,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 +212,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@1d820463733701cf1484c7eb5d7d24a15ca2c454 # v1.2.1
env:
SM_API_KEY: ${{ secrets.SM_API_KEY }}
SM_CLIENT_CERT_FILE_B64: ${{ secrets.SM_CLIENT_CERT_FILE_B64 }}
@@ -255,7 +287,7 @@ jobs:
}
- name: Import Apple Developer Certificate
if: matrix.platform == 'macos-15' || matrix.platform == 'macos-15-intel'
if: (matrix.platform == 'macos-15' || matrix.platform == 'macos-15-intel') && env.APPLE_CERTIFICATE != ''
env:
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
@@ -276,7 +308,7 @@ jobs:
rm certificate.p12
- name: Verify Certificate
if: matrix.platform == 'macos-15' || matrix.platform == 'macos-15-intel'
if: (matrix.platform == 'macos-15' || matrix.platform == 'macos-15-intel') && env.APPLE_CERTIFICATE != ''
run: |
echo "Verifying Apple Developer Certificate..."
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
@@ -299,7 +331,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 }}
@@ -311,8 +343,9 @@ jobs:
APPIMAGETOOL_SIGN_PASSPHRASE: ${{ secrets.APPIMAGETOOL_SIGN_PASSPHRASE }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY: ${{ secrets.VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY }}
VITE_SAAS_SERVER_URL: ${{ secrets.VITE_SAAS_SERVER_URL }}
VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY: ${{ secrets.VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY || 'sb_publishable_UHz2SVRF5mvdrPHWkRteyA_yNlZTkYb' }}
VITE_SAAS_SERVER_URL: ${{ secrets.VITE_SAAS_SERVER_URL || 'https://app.stirlingpdf.com' }}
VITE_SAAS_BACKEND_API_URL: ${{ secrets.VITE_SAAS_BACKEND_API_URL || 'https://api.stirlingpdf.com' }}
# Only enable Windows signing in Tauri when on main
SIGN: ${{ github.ref == 'refs/heads/main' && (env.SM_API_KEY == '' && env.WINDOWS_CERTIFICATE != '') && '1' || '0' }}
CI: true
@@ -573,7 +606,7 @@ jobs:
}
- name: Upload artifacts
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: Stirling-PDF-${{ matrix.name }}
path: ./dist/*
@@ -626,11 +659,110 @@ jobs:
fi
done
pr-comment:
needs: build
runs-on: ubuntu-latest
if: github.event_name == 'pull_request' && needs.build.result == 'success'
permissions:
pull-requests: write
steps:
- name: Harden the runner
uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1
with:
egress-policy: audit
- name: Post/Update PR Comment with Download Links
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: |
const owner = context.repo.owner;
const repo = context.repo.repo;
const prNumber = context.issue.number;
const runId = context.runId;
// Fetch artifacts for this workflow run
const { data: artifactsList } = await github.rest.actions.listWorkflowRunArtifacts({
owner,
repo,
run_id: runId
});
// Map of expected artifact names to display info
const artifactMap = {
'Stirling-PDF-windows-x86_64': { icon: '🪟', platform: 'Windows x64', files: '.exe, .msi' },
'Stirling-PDF-macos-aarch64': { icon: '🍎', platform: 'macOS ARM64', files: '.dmg' },
'Stirling-PDF-macos-x86_64': { icon: '🍎', platform: 'macOS Intel', files: '.dmg' },
'Stirling-PDF-linux-x86_64': { icon: '🐧', platform: 'Linux x64', files: '.deb, .AppImage' }
};
let commentBody = `## 📦 Tauri Desktop Builds Ready!\n\n`;
commentBody += `The desktop applications have been built and are ready for testing.\n\n`;
commentBody += `### Download Artifacts:\n\n`;
// Add links for each found artifact
let foundArtifacts = 0;
for (const artifact of artifactsList.artifacts) {
const info = artifactMap[artifact.name];
if (info) {
foundArtifacts++;
// GitHub doesn't provide direct download URLs via API, but we can link to the artifact on the Actions page
const artifactUrl = `https://github.com/${owner}/${repo}/actions/runs/${runId}/artifacts/${artifact.id}`;
commentBody += `${info.icon} **${info.platform}**: [Download ${artifact.name}](${artifactUrl}) `;
commentBody += `(${info.files}) - ${(artifact.size_in_bytes / 1024 / 1024).toFixed(1)} MB\n`;
}
}
if (foundArtifacts === 0) {
commentBody += `⚠️ **Warning**: No artifacts found in workflow run.\n`;
commentBody += `[View workflow run](https://github.com/${owner}/${repo}/actions/runs/${runId})\n`;
}
commentBody += `\n---\n`;
commentBody += `_Built from commit ${context.sha.substring(0, 7)}_\n`;
commentBody += `_Artifacts expire in 7 days_`;
// Find existing comment
const { data: comments } = await github.rest.issues.listComments({
owner,
repo,
issue_number: prNumber
});
const botComment = comments.find(comment =>
comment.user.type === 'Bot' &&
comment.body.includes('📦 Tauri Desktop Builds Ready!')
);
if (botComment) {
// Update existing comment
await github.rest.issues.updateComment({
owner,
repo,
comment_id: botComment.id,
body: commentBody
});
console.log('Updated existing comment');
} else {
// Create new comment
await github.rest.issues.createComment({
owner,
repo,
issue_number: prNumber,
body: commentBody
});
console.log('Created new comment');
}
report:
needs: build
runs-on: ubuntu-latest
if: always()
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1
with:
egress-policy: audit
- name: Report build results
run: |
if [ "${{ needs.build.result }}" = "success" ]; then
+25 -15
View File
@@ -25,27 +25,30 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up JDK 21
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
- name: Set up JDK 25
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
java-version: "21"
java-version: "25"
distribution: "temurin"
- name: Setup Gradle
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
with:
gradle-version: 8.14
gradle-version: 9.3.1
- 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
@@ -58,7 +61,7 @@ jobs:
echo "versionNumber=$VERSION" >> $GITHUB_OUTPUT
- name: Login to Docker Hub
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_API }}
@@ -69,6 +72,8 @@ jobs:
context: .
file: ./docker/embedded/Dockerfile
push: true
cache-from: type=gha,scope=stirling-pdf-latest
cache-to: type=gha,mode=max,scope=stirling-pdf-latest
tags: ${{ secrets.DOCKER_HUB_USERNAME }}/test:test-${{ github.sha }}
build-args: VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
platforms: linux/amd64
@@ -125,10 +130,15 @@ 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@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1
with:
egress-policy: audit
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Check for file changes
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
id: changes
with:
filters: ".github/config/.files.yaml"
@@ -140,14 +150,14 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1
with:
egress-policy: audit
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Node
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
cache: "npm"
cache-dependency-path: frontend/package-lock.json
@@ -176,7 +186,7 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1
with:
egress-policy: audit
+3 -2
View File
@@ -149,6 +149,7 @@ app/proprietary/build
common/build
proprietary/build
stirling-pdf/build
frontend/src-tauri/provisioner/target
# Byte-compiled / optimized / DLL files
__pycache__/
@@ -157,6 +158,7 @@ __pycache__/
# Virtual environments
.env*
!.env*.example
.venv*
env*/
venv*/
@@ -213,7 +215,7 @@ id_ed25519.pub
.mypy_cache
.pytest_cache
.ipynb_checkpoints
.build-cache
**/jcef-bundle/
@@ -243,4 +245,3 @@ docs/type3/signatures/
# Type3 sample PDFs (development only)
**/type3/samples/
+9 -3
View File
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.14.8
rev: v0.14.14
hooks:
- id: ruff
args:
@@ -16,11 +16,11 @@ repos:
hooks:
- id: codespell
args:
- --ignore-words-list=thirdParty,tabEl,tabEls
- --ignore-words-list=thirdParty,tabEl,tabEls,Sie,ist
- --skip="./.*,*.csv,*.json,*.ambr"
- --quiet-level=2
files: \.(html|css|js|py|md)$
exclude: (.vscode|.devcontainer|app/core/src/main/resources|app/proprietary/src/main/resources|Dockerfile|.*/pdfjs.*|.*/thirdParty.*|bootstrap.*|.*\.min\..*|.*diff\.js)
exclude: (.vscode|.devcontainer|app/core/src/main/resources|app/proprietary/src/main/resources|frontend/public/vendor|Dockerfile|.*/pdfjs.*|.*/thirdParty.*|bootstrap.*|.*\.min\..*|.*diff\.js)
- repo: https://github.com/gitleaks/gitleaks
rev: v8.30.0
hooks:
@@ -34,6 +34,12 @@ repos:
- id: trailing-whitespace
files: ^.*(\.js|\.java|\.py|\.yml)$
exclude: ^(.*/pdfjs.*|.*/thirdParty.*|bootstrap.*|.*\.min\..*|.*diff\.js|\.github/workflows/.*$)
- repo: https://github.com/pappasam/toml-sort
rev: v0.24.3
hooks:
- id: toml-sort-fix
files: frontend/public/locales/.*\.toml$
args: ['--in-place', '--all', '--ignore-case']
# - repo: https://github.com/thibaudcolas/pre-commit-stylelint
# rev: v16.21.1
# hooks:
+1 -1
View File
@@ -13,7 +13,7 @@
"vscjava.vscode-spring-boot-dashboard", // Spring Boot dashboard for managing and visualizing Spring Boot applications
"EditorConfig.EditorConfig", // EditorConfig support for maintaining consistent coding styles
"ms-azuretools.vscode-docker", // Docker extension for Visual Studio Code
"GitHub.copilot", // GitHub Copilot AI pair programmer for Visual Studio Code
"GitHub.copilot-chat", // GitHub Copilot AI pair programmer for Visual Studio Code
"GitHub.vscode-pull-request-github", // GitHub Pull Requests extension for Visual Studio Code
"charliermarsh.ruff", // Ruff code formatter for Python to follow the Ruff Style Guide
"yzhang.markdown-all-in-one", // Markdown All-in-One extension for enhanced Markdown editing
+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
+361
View File
@@ -0,0 +1,361 @@
# AGENTS.md
This file provides guidance to AI Agents when working with code in this repository.
## Common Development Commands
### Build and Test
- **Build project**: `./gradlew clean build`
- **Run locally**: `./gradlew bootRun`
- **Full test suite**: `./test.sh` (builds all Docker variants and runs comprehensive tests)
- **Code formatting**: `./gradlew spotlessApply` (runs automatically before compilation)
### Docker Development
- **Build ultra-lite**: `docker build -t stirlingtools/stirling-pdf:latest-ultra-lite -f ./Dockerfile.ultra-lite .`
- **Build standard**: `docker build -t stirlingtools/stirling-pdf:latest -f ./Dockerfile .`
- **Build fat version**: `docker build -t stirlingtools/stirling-pdf:latest-fat -f ./Dockerfile.fat .`
- **Example compose files**: Located in `exampleYmlFiles/` directory
### Security Mode Development
Set `DOCKER_ENABLE_SECURITY=true` environment variable to enable security features during development. This is required for testing the full version locally.
### Python Development
Development for the AI engine happens in the `engine/` folder. It's built with Langchain and Pydantic and allows for the creation and editing of PDF documents. The frontend calls the Python via Java as a proxy.
- Python version is 3.13; use modern Python features (type aliases, pattern matching, dataclasses, etc.) where they help clarity.
- Write fully type-correct code; keep pyright clean and avoid `Any` unless strictly necessary.
- JSON handling: deserialize into fully typed Pydantic models as early as possible, and serialize back from Pydantic models as late as possible.
- Use Makefile commands for Python work:
- From `engine/`: `make check` to lint, type-check, test, etc. and `make fix` to fix easily fixable linting & formatting issues.
- The project structure is defined in `engine/pyproject.toml`. Any new dependencies should be listed appropriately there, followed by running `make install`.
- Prefer using classes to nesting functions, and make other similar architectural decisions to improve testability. Do not nest classes or functions unless specifically required to for the code construct (like a decorator).
- All environment variables used within the code must begin with the `STIRLING_` prefix in order to keep them unique and easier to find.
### Frontend Development
- **Frontend dev server**: `cd frontend && npm run dev` (requires backend on localhost:8080)
- **Tech Stack**: Vite + React + TypeScript + Mantine UI + TailwindCSS
- **Proxy Configuration**: Vite proxies `/api/*` calls to backend (localhost:8080)
- **Build Process**: DO NOT run build scripts manually - builds are handled by CI/CD pipelines
- **Package Installation**: DO NOT run npm install commands - package management handled separately
- **Deployment Options**:
- **Desktop App**: `npm run tauri-build` (native desktop application)
- **Web Server**: `npm run build` then serve dist/ folder
- **Development**: `npm run tauri-dev` for desktop dev mode
#### Environment Variables
- All `VITE_*` variables must be declared in the appropriate example file:
- `frontend/config/.env.example` — core, proprietary, and shared vars
- `frontend/config/.env.saas.example` — SaaS-only vars
- `frontend/config/.env.desktop.example` — desktop (Tauri)-only vars
- Never use `|| 'hardcoded-fallback'` inline — put defaults in the example files
- `npm run prep` / `prep:saas` / `prep:desktop` auto-create the env files from examples on first run, and error if any required keys are missing
- These prep scripts run automatically at the start of all `dev*`, `build*`, and `tauri*` commands
- See `frontend/README.md#environment-variables` for full documentation
#### Import Paths - CRITICAL
**ALWAYS use `@app/*` for imports.** Do not use `@core/*` or `@proprietary/*` unless explicitly wrapping/extending a lower layer implementation.
For a broader explanation of the frontend layering and override architecture, see [frontend/DeveloperGuide.md](frontend/DeveloperGuide.md).
```typescript
// ✅ CORRECT - Use @app/* for all imports
import { AppLayout } from "@app/components/AppLayout";
import { useFileContext } from "@app/contexts/FileContext";
import { FileContext } from "@app/contexts/FileContext";
// ❌ WRONG - Do not use @core/* or @proprietary/* in normal code
import { AppLayout } from "@core/components/AppLayout";
import { useFileContext } from "@proprietary/contexts/FileContext";
```
**Only use explicit aliases when:**
- Building layer-specific override that wraps a lower layer's component
- Example: `import { AppProviders as CoreAppProviders } from "@core/components/AppProviders"` when creating proprietary/AppProviders.tsx that extends the core version
The `@app/*` alias automatically resolves to the correct layer based on build target (core/proprietary/desktop) and handles the fallback cascade.
#### Component Override Pattern (Stub/Shadow)
Use this pattern for desktop-specific or proprietary-specific features WITHOUT runtime checks or conditionals.
**How it works:**
1. Core defines stub component (returns null or no-op)
2. Desktop/proprietary overrides with same path/name
3. Core imports via `@app/*` - higher layer "shadows" core in those builds
4. No `@ts-ignore`, no `isTauri()` checks, no runtime conditionals!
**Example - Desktop-specific footer:**
```typescript
// core/components/rightRail/RightRailFooterExtensions.tsx (stub)
interface RightRailFooterExtensionsProps {
className?: string;
}
export function RightRailFooterExtensions(_props: RightRailFooterExtensionsProps) {
return null; // Stub - does nothing in web builds
}
```
```tsx
// desktop/components/rightRail/RightRailFooterExtensions.tsx (real implementation)
import { Box } from '@mantine/core';
import { BackendHealthIndicator } from '@app/components/BackendHealthIndicator';
interface RightRailFooterExtensionsProps {
className?: string;
}
export function RightRailFooterExtensions({ className }: RightRailFooterExtensionsProps) {
return (
<Box className={className}>
<BackendHealthIndicator />
</Box>
);
}
```
```tsx
// core/components/shared/RightRail.tsx (usage - works in ALL builds)
import { RightRailFooterExtensions } from '@app/components/rightRail/RightRailFooterExtensions';
export function RightRail() {
return (
<div>
{/* In web builds: renders nothing (stub returns null) */}
{/* In desktop builds: renders BackendHealthIndicator */}
<RightRailFooterExtensions className="right-rail-footer" />
</div>
);
}
```
**Build resolution:**
- **Core build**: `@app/*``core/*` → Gets stub (returns null)
- **Desktop build**: `@app/*``desktop/*` → Gets real implementation (shadows core)
**Benefits:**
- No runtime checks or feature flags
- Type-safe across all builds
- Clean, readable code
- Build-time optimization (dead code elimination)
#### Multi-Tool Workflow Architecture
Frontend designed for **stateful document processing**:
- Users upload PDFs once, then chain tools (split → merge → compress → view)
- File state and processing results persist across tool switches
- No file reloading between tools - performance critical for large PDFs (up to 100GB+)
#### FileContext - Central State Management
**Location**: `frontend/src/core/contexts/FileContext.tsx`
- **Active files**: Currently loaded PDFs and their variants
- **Tool navigation**: Current mode (viewer/pageEditor/fileEditor/toolName)
- **Memory management**: PDF document cleanup, blob URL lifecycle, Web Worker management
- **IndexedDB persistence**: File storage with thumbnail caching
- **Preview system**: Tools can preview results (e.g., Split → Viewer → back to Split) without context pollution
**Critical**: All file operations go through FileContext. Don't bypass with direct file handling.
#### Processing Services
- **enhancedPDFProcessingService**: Background PDF parsing and manipulation
- **thumbnailGenerationService**: Web Worker-based with main-thread fallback
- **fileStorage**: IndexedDB with LRU cache management
#### Memory Management Strategy
**Why manual cleanup exists**: Large PDFs (up to 100GB+) through multiple tools accumulate:
- PDF.js documents that need explicit .destroy() calls
- Blob URLs from tool outputs that need revocation
- Web Workers that need termination
Without cleanup: browser crashes with memory leaks.
#### Tool Development
**Architecture**: Modular hook-based system with clear separation of concerns:
- **useToolOperation** (`frontend/src/core/hooks/tools/shared/useToolOperation.ts`): Main orchestrator hook
- Coordinates all tool operations with consistent interface
- Integrates with FileContext for operation tracking
- Handles validation, error handling, and UI state management
- **Supporting Hooks**:
- **useToolState**: UI state management (loading, progress, error, files)
- **useToolApiCalls**: HTTP requests and file processing
- **useToolResources**: Blob URLs, thumbnails, ZIP downloads
- **Utilities**:
- **toolErrorHandler**: Standardized error extraction and i18n support
- **toolResponseProcessor**: API response handling (single/zip/custom)
- **toolOperationTracker**: FileContext integration utilities
**Three Tool Patterns**:
**Pattern 1: Single-File Tools** (Individual processing)
- Backend processes one file per API call
- Set `multiFileEndpoint: false`
- Examples: Compress, Rotate
```typescript
return useToolOperation({
operationType: 'compress',
endpoint: '/api/v1/misc/compress-pdf',
buildFormData: (params, file: File) => { /* single file */ },
multiFileEndpoint: false,
});
```
**Pattern 2: Multi-File Tools** (Batch processing)
- Backend accepts `MultipartFile[]` arrays in single API call
- Set `multiFileEndpoint: true`
- Examples: Split, Merge, Overlay
```typescript
return useToolOperation({
operationType: 'split',
endpoint: '/api/v1/general/split-pages',
buildFormData: (params, files: File[]) => { /* all files */ },
multiFileEndpoint: true,
filePrefix: 'split_',
});
```
**Pattern 3: Complex Tools** (Custom processing)
- Tools with complex routing logic or non-standard processing
- Provide `customProcessor` for full control
- Examples: Convert, OCR
```typescript
return useToolOperation({
operationType: 'convert',
customProcessor: async (params, files) => { /* custom logic */ },
});
```
**Benefits**:
- **No Timeouts**: Operations run until completion (supports 100GB+ files)
- **Consistent**: All tools follow same pattern and interface
- **Maintainable**: Single responsibility hooks, easy to test and modify
- **i18n Ready**: Built-in internationalization support
- **Type Safe**: Full TypeScript support with generic interfaces
- **Memory Safe**: Automatic resource cleanup and blob URL management
## Architecture Overview
### Project Structure
- **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
- **Security**: Spring Security with optional authentication (controlled by `DOCKER_ENABLE_SECURITY`)
- **Configuration**: YAML-based configuration with environment variable overrides
### Controller Architecture
- **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/...")`
### Key Components
- **SPDFApplication.java**: Main application class with desktop UI and browser launching logic
- **ConfigInitializer**: Handles runtime configuration and settings files
- **Pipeline System**: Automated PDF processing workflows via `PipelineController`
- **Security Layer**: Authentication, authorization, and user management (when enabled)
### Frontend Directory Structure
The frontend is organized with a clear separation of concerns:
- **`frontend/src/core/`**: Main application code (shared, production-ready components)
- **`core/components/`**: React components organized by feature
- `core/components/tools/`: Individual PDF tool implementations
- `core/components/viewer/`: PDF viewer components
- `core/components/pageEditor/`: Page manipulation UI
- `core/components/tooltips/`: Help tooltips for tools
- `core/components/shared/`: Reusable UI components
- **`core/contexts/`**: React Context providers
- `FileContext.tsx`: Central file state management
- `file/`: File reducer and selectors
- `toolWorkflow/`: Tool workflow state
- **`core/hooks/`**: Custom React hooks
- `hooks/tools/`: Tool-specific operation hooks (one directory per tool)
- `hooks/tools/shared/`: Shared hook utilities (useToolOperation, etc.)
- **`core/constants/`**: Application constants and configuration
- **`core/data/`**: Static data (tool taxonomy, etc.)
- **`core/services/`**: Business logic services (PDF processing, storage, etc.)
- **`frontend/src/desktop/`**: Desktop-specific (Tauri) code
- **`frontend/src/proprietary/`**: Proprietary/licensed features
- **`frontend/src-tauri/`**: Tauri (Rust) native desktop application code
- **`frontend/public/`**: Static assets served directly
- `public/locales/`: Translation JSON files
### Component Architecture
- **Static Assets**: CSS, JS, and resources in `src/main/resources/static/` (legacy) + `frontend/public/` (modern)
- **Internationalization**:
- Backend: `messages_*.properties` files
- Frontend: JSON files in `frontend/public/locales/` (converted from .properties)
- Conversion Script: `scripts/convert_properties_to_json.py`
### Configuration Modes
- **Ultra-lite**: Basic PDF operations only
- **Standard**: Full feature set
- **Fat**: Pre-downloaded dependencies for air-gapped environments
- **Security Mode**: Adds authentication, user management, and enterprise features
### Testing Strategy
- **Integration Tests**: Cucumber tests in `testing/cucumber/`
- **Docker Testing**: `test.sh` validates all Docker variants
- **Manual Testing**: No unit tests currently - relies on UI and API testing
## Development Workflow
1. **Local Development**:
- Backend: `./gradlew bootRun` (runs on localhost:8080)
- Frontend: `cd frontend && npm run dev` (runs on localhost:5173, proxies to backend)
2. **Docker Testing**: Use `./test.sh` before submitting PRs
3. **Code Style**: Spotless enforces Google Java Format automatically
4. **Translations**:
- Backend: Use helper scripts in `/scripts` for multi-language updates
- Frontend: Update JSON files in `frontend/public/locales/` or use conversion script
5. **Documentation**: API docs auto-generated and available at `/swagger-ui/index.html`
## Frontend Architecture Status
- **Core Status**: React SPA architecture complete with multi-tool workflow support
- **State Management**: FileContext handles all file operations and tool navigation
- **File Processing**: Production-ready with memory management for large PDF workflows (up to 100GB+)
- **Tool Integration**: Modular hook architecture with `useToolOperation` orchestrator
- Individual hooks: `useToolState`, `useToolApiCalls`, `useToolResources`
- Utilities: `toolErrorHandler`, `toolResponseProcessor`, `toolOperationTracker`
- Pattern: Each tool creates focused operation hook, UI consumes state/actions
- **Preview System**: Tool results can be previewed without polluting file context (Split tool example)
- **Performance**: Web Worker thumbnails, IndexedDB persistence, background processing
## Translation Rules
- **CRITICAL**: Always update translations in `en-GB` only, never `en-US`
- Translation files are located in `frontend/public/locales/`
## Important Notes
- **Java Version**: Minimum JDK 21, supports and recommends JDK 25
- **Lombok**: Used extensively - ensure IDE plugin is installed
- **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)
- **Security**: When `DOCKER_ENABLE_SECURITY=false`, security-related classes are excluded from compilation
- **Import Paths**: ALWAYS use `@app/*` for imports - never use `@core/*` or `@proprietary/*` unless explicitly wrapping/extending a lower layer
- **FileContext**: All file operations MUST go through FileContext - never bypass with direct File handling
- **Memory Management**: Manual cleanup required for PDF.js documents and blob URLs - don't remove cleanup code
- **Tool Development**: New tools should follow `useToolOperation` hook pattern (see `useCompressOperation.ts`)
- **Performance Target**: Must handle PDFs up to 100GB+ without browser crashes
- **Preview System**: Tools can preview results without polluting main file context (see Split tool implementation)
- **Adding Tools**: See `ADDING_TOOLS.md` for complete guide to creating new PDF tools
## Communication Style
- Be direct and to the point
- No apologies or conversational filler
- Answer questions directly without preamble
- Explain reasoning concisely when asked
- Avoid unnecessary elaboration
## Decision Making
- Ask clarifying questions before making assumptions
- Stop and ask when uncertain about project-specific details
- Confirm approach before making structural changes
- Request guidance on preferences (cross-platform vs specific tools, etc.)
- Verify understanding of requirements before proceeding
-228
View File
@@ -1,228 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Common Development Commands
### Build and Test
- **Build project**: `./gradlew clean build`
- **Run locally**: `./gradlew bootRun`
- **Full test suite**: `./test.sh` (builds all Docker variants and runs comprehensive tests)
- **Code formatting**: `./gradlew spotlessApply` (runs automatically before compilation)
### Docker Development
- **Build ultra-lite**: `docker build -t stirlingtools/stirling-pdf:latest-ultra-lite -f ./Dockerfile.ultra-lite .`
- **Build standard**: `docker build -t stirlingtools/stirling-pdf:latest -f ./Dockerfile .`
- **Build fat version**: `docker build -t stirlingtools/stirling-pdf:latest-fat -f ./Dockerfile.fat .`
- **Example compose files**: Located in `exampleYmlFiles/` directory
### Security Mode Development
Set `DOCKER_ENABLE_SECURITY=true` environment variable to enable security features during development. This is required for testing the full version locally.
### Frontend Development
- **Frontend dev server**: `cd frontend && npm run dev` (requires backend on localhost:8080)
- **Tech Stack**: Vite + React + TypeScript + Mantine UI + TailwindCSS
- **Proxy Configuration**: Vite proxies `/api/*` calls to backend (localhost:8080)
- **Build Process**: DO NOT run build scripts manually - builds are handled by CI/CD pipelines
- **Package Installation**: DO NOT run npm install commands - package management handled separately
- **Deployment Options**:
- **Desktop App**: `npm run tauri-build` (native desktop application)
- **Web Server**: `npm run build` then serve dist/ folder
- **Development**: `npm run tauri-dev` for desktop dev mode
#### Multi-Tool Workflow Architecture
Frontend designed for **stateful document processing**:
- Users upload PDFs once, then chain tools (split → merge → compress → view)
- File state and processing results persist across tool switches
- No file reloading between tools - performance critical for large PDFs (up to 100GB+)
#### FileContext - Central State Management
**Location**: `src/contexts/FileContext.tsx`
- **Active files**: Currently loaded PDFs and their variants
- **Tool navigation**: Current mode (viewer/pageEditor/fileEditor/toolName)
- **Memory management**: PDF document cleanup, blob URL lifecycle, Web Worker management
- **IndexedDB persistence**: File storage with thumbnail caching
- **Preview system**: Tools can preview results (e.g., Split → Viewer → back to Split) without context pollution
**Critical**: All file operations go through FileContext. Don't bypass with direct file handling.
#### Processing Services
- **enhancedPDFProcessingService**: Background PDF parsing and manipulation
- **thumbnailGenerationService**: Web Worker-based with main-thread fallback
- **fileStorage**: IndexedDB with LRU cache management
#### Memory Management Strategy
**Why manual cleanup exists**: Large PDFs (up to 100GB+) through multiple tools accumulate:
- PDF.js documents that need explicit .destroy() calls
- Blob URLs from tool outputs that need revocation
- Web Workers that need termination
Without cleanup: browser crashes with memory leaks.
#### Tool Development
**Architecture**: Modular hook-based system with clear separation of concerns:
- **useToolOperation** (`frontend/src/hooks/tools/shared/useToolOperation.ts`): Main orchestrator hook
- Coordinates all tool operations with consistent interface
- Integrates with FileContext for operation tracking
- Handles validation, error handling, and UI state management
- **Supporting Hooks**:
- **useToolState**: UI state management (loading, progress, error, files)
- **useToolApiCalls**: HTTP requests and file processing
- **useToolResources**: Blob URLs, thumbnails, ZIP downloads
- **Utilities**:
- **toolErrorHandler**: Standardized error extraction and i18n support
- **toolResponseProcessor**: API response handling (single/zip/custom)
- **toolOperationTracker**: FileContext integration utilities
**Three Tool Patterns**:
**Pattern 1: Single-File Tools** (Individual processing)
- Backend processes one file per API call
- Set `multiFileEndpoint: false`
- Examples: Compress, Rotate
```typescript
return useToolOperation({
operationType: 'compress',
endpoint: '/api/v1/misc/compress-pdf',
buildFormData: (params, file: File) => { /* single file */ },
multiFileEndpoint: false,
});
```
**Pattern 2: Multi-File Tools** (Batch processing)
- Backend accepts `MultipartFile[]` arrays in single API call
- Set `multiFileEndpoint: true`
- Examples: Split, Merge, Overlay
```typescript
return useToolOperation({
operationType: 'split',
endpoint: '/api/v1/general/split-pages',
buildFormData: (params, files: File[]) => { /* all files */ },
multiFileEndpoint: true,
filePrefix: 'split_',
});
```
**Pattern 3: Complex Tools** (Custom processing)
- Tools with complex routing logic or non-standard processing
- Provide `customProcessor` for full control
- Examples: Convert, OCR
```typescript
return useToolOperation({
operationType: 'convert',
customProcessor: async (params, files) => { /* custom logic */ },
});
```
**Benefits**:
- **No Timeouts**: Operations run until completion (supports 100GB+ files)
- **Consistent**: All tools follow same pattern and interface
- **Maintainable**: Single responsibility hooks, easy to test and modify
- **i18n Ready**: Built-in internationalization support
- **Type Safe**: Full TypeScript support with generic interfaces
- **Memory Safe**: Automatic resource cleanup and blob URL management
## Architecture Overview
### Project Structure
- **Backend**: Spring Boot application with Thymeleaf templating
- **Frontend**: React-based SPA in `/frontend` directory (Thymeleaf templates fully replaced)
- **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
- **Security**: Spring Security with optional authentication (controlled by `DOCKER_ENABLE_SECURITY`)
- **Configuration**: YAML-based configuration with environment variable overrides
### Controller Architecture
- **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
- **ConfigInitializer**: Handles runtime configuration and settings files
- **Pipeline System**: Automated PDF processing workflows via `PipelineController`
- **Security Layer**: Authentication, authorization, and user management (when enabled)
### Component Architecture
- **React Components**: Located in `frontend/src/components/` and `frontend/src/tools/`
- **Static Assets**: CSS, JS, and resources in `src/main/resources/static/` (legacy) + `frontend/public/` (modern)
- **Internationalization**:
- Backend: `messages_*.properties` files
- Frontend: JSON files in `frontend/public/locales/` (converted from .properties)
- Conversion Script: `scripts/convert_properties_to_json.py`
### Configuration Modes
- **Ultra-lite**: Basic PDF operations only
- **Standard**: Full feature set
- **Fat**: Pre-downloaded dependencies for air-gapped environments
- **Security Mode**: Adds authentication, user management, and enterprise features
### Testing Strategy
- **Integration Tests**: Cucumber tests in `testing/cucumber/`
- **Docker Testing**: `test.sh` validates all Docker variants
- **Manual Testing**: No unit tests currently - relies on UI and API testing
## Development Workflow
1. **Local Development**:
- Backend: `./gradlew bootRun` (runs on localhost:8080)
- Frontend: `cd frontend && npm run dev` (runs on localhost:5173, proxies to backend)
2. **Docker Testing**: Use `./test.sh` before submitting PRs
3. **Code Style**: Spotless enforces Google Java Format automatically
4. **Translations**:
- Backend: Use helper scripts in `/scripts` for multi-language updates
- Frontend: Update JSON files in `frontend/public/locales/` or use conversion script
5. **Documentation**: API docs auto-generated and available at `/swagger-ui/index.html`
## Frontend Architecture Status
- **Core Status**: React SPA architecture complete with multi-tool workflow support
- **State Management**: FileContext handles all file operations and tool navigation
- **File Processing**: Production-ready with memory management for large PDF workflows (up to 100GB+)
- **Tool Integration**: Modular hook architecture with `useToolOperation` orchestrator
- Individual hooks: `useToolState`, `useToolApiCalls`, `useToolResources`
- Utilities: `toolErrorHandler`, `toolResponseProcessor`, `toolOperationTracker`
- Pattern: Each tool creates focused operation hook, UI consumes state/actions
- **Preview System**: Tool results can be previewed without polluting file context (Split tool example)
- **Performance**: Web Worker thumbnails, IndexedDB persistence, background processing
## Translation Rules
- **CRITICAL**: Always update translations in `en-GB` only, never `en-US`
- Translation files are located in `frontend/public/locales/`
## Important Notes
- **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)
- **Security**: When `DOCKER_ENABLE_SECURITY=false`, security-related classes are excluded from compilation
- **FileContext**: All file operations MUST go through FileContext - never bypass with direct File handling
- **Memory Management**: Manual cleanup required for PDF.js documents and blob URLs - don't remove cleanup code
- **Tool Development**: New tools should follow `useToolOperation` hook pattern (see `useCompressOperation.ts`)
- **Performance Target**: Must handle PDFs up to 100GB+ without browser crashes
- **Preview System**: Tools can preview results without polluting main file context (see Split tool implementation)
- **Adding Tools**: See `ADDING_TOOLS.md` for complete guide to creating new PDF tools
## Communication Style
- Be direct and to the point
- No apologies or conversational filler
- Answer questions directly without preamble
- Explain reasoning concisely when asked
- Avoid unnecessary elaboration
## Decision Making
- Ask clarifying questions before making assumptions
- Stop and ask when uncertain about project-specific details
- Confirm approach before making structural changes
- Request guidance on preferences (cross-platform vs specific tools, etc.)
- Verify understanding of requirements before proceeding
Symlink
+1
View File
@@ -0,0 +1 @@
AGENTS.md
+6 -165
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.
@@ -11,7 +11,7 @@ This guide focuses on developing for Stirling 2.0, including both the React fron
**Stirling 2.0** is built using:
**Backend:**
- Spring Boot (Java 17+, JDK 21 recommended)
- Spring Boot (Java 21+, JDK 25 recommended)
- PDFBox for core PDF operations
- LibreOffice for document conversions
- qpdf for PDF optimization
@@ -38,16 +38,13 @@ 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
- Docker
- Git
- Java JDK 17 or later (JDK 21 recommended)
- Java JDK 21 or later (JDK 25 recommended)
- Node.js 18+ and npm (required for frontend development)
- Gradle 7.0 or later (Included within the repo)
- Rust and Cargo (required for Tauri desktop app development)
@@ -62,7 +59,7 @@ This guide focuses on developing for Stirling 2.0, including both the React fron
cd Stirling-PDF
```
2. Install Docker and JDK17 if not already installed.
2. Install Docker and JDK 21 (or JDK 25 recommended) if not already installed.
3. Install a recommended Java IDE such as Eclipse, IntelliJ, or VSCode
1. Only VSCode
@@ -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.
+4
View File
@@ -6,10 +6,14 @@ Portions of this software are licensed as follows:
* All content that resides under the "app/proprietary/" directory of this repository,
if that directory exists, is licensed under the license defined in "app/proprietary/LICENSE".
* All content that resides under the "engine/" directory of this repository,
if that directory exists, is licensed under the license defined in "engine/LICENSE".
* All content that resides under the "frontend/src/proprietary/" directory of this repository,
if that directory exists, is licensed under the license defined in "frontend/src/proprietary/LICENSE".
* All content that resides under the "frontend/src/desktop/" directory of this repository,
if that directory exists, is licensed under the license defined in "frontend/src/desktop/LICENSE".
* All content that resides under the "frontend/src/saas/" directory of this repository,
if that directory exists, is licensed under the license defined in "frontend/src/saas/LICENSE".
* Content outside of the above mentioned directories or restrictions above is
available under the MIT License as defined below.
+8 -8
View File
@@ -5,10 +5,10 @@ bootRun {
spotless {
java {
target 'src/**/java/**/*.java'
targetExclude 'src/main/java/org/apache/**'
googleJavaFormat(googleJavaFormatVersion).aosp().reorderImports(false)
importOrder("java", "javax", "org", "com", "net", "io", "jakarta", "lombok", "me", "stirling")
toggleOffOn()
trimTrailingWhitespace()
leadingTabsToSpaces()
endWithNewline()
@@ -27,10 +27,10 @@ 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.google.guava:guava:33.4.8-jre'
api 'org.springframework.boot:spring-boot-starter-webmvc'
api 'org.springframework.boot:spring-boot-starter-aspectj'
api 'com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20260102.1'
api 'com.fathzer:javaluator:3.0.6'
api 'com.posthog.java:posthog:1.2.0'
api 'org.apache.commons:commons-lang3:3.20.0'
@@ -40,10 +40,10 @@ dependencies {
api "org.apache.pdfbox:pdfbox-io:$pdfboxVersion"
api "org.apache.pdfbox:xmpbox:$pdfboxVersion"
api "org.apache.pdfbox:preflight:$pdfboxVersion"
api 'com.github.junrar:junrar:7.5.7' // RAR archive support for CBR files
api 'com.github.junrar:junrar:7.5.8' // 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:3.0.1"
// 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
@@ -103,8 +103,9 @@ public class EndpointConfiguration {
// Rule 2: Functional-group override - check if endpoint belongs to any disabled functional
// group
for (String group : endpointGroups.keySet()) {
if (disabledGroups.contains(group) && endpointGroups.get(group).contains(endpoint)) {
for (Map.Entry<String, Set<String>> entry : endpointGroups.entrySet()) {
String group = entry.getKey();
if (disabledGroups.contains(group) && entry.getValue().contains(endpoint)) {
// Skip tool groups (qpdf, OCRmyPDF, Ghostscript, LibreOffice, etc.)
if (!isToolGroup(group)) {
log.debug(
@@ -131,10 +132,11 @@ public class EndpointConfiguration {
// Rule 4: Single-dependency check - if no alternatives defined, check if endpoint belongs
// to any disabled tool groups
for (String group : endpointGroups.keySet()) {
for (Map.Entry<String, Set<String>> entry : endpointGroups.entrySet()) {
String group = entry.getKey();
if (isToolGroup(group)
&& disabledGroups.contains(group)
&& endpointGroups.get(group).contains(endpoint)) {
&& entry.getValue().contains(endpoint)) {
log.debug(
"isEndpointEnabled('{}') -> false (single tool group '{}' disabled, no alternatives)",
original,
@@ -303,22 +305,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 +337,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 +346,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");
@@ -350,52 +356,61 @@ public class EndpointConfiguration {
addEndpointToGroup("Security", "cert-sign");
addEndpointToGroup("Security", "remove-cert-sign");
addEndpointToGroup("Security", "sanitize-pdf");
addEndpointToGroup("Security", "timestamp-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", "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");
// Adding endpoints to "Automation" group
addEndpointToGroup("Automation", "handleData");
addEndpointToGroup("Automation", "automate"); // Alias for handleData (user-friendly name)
addEndpointToGroup("Automation", "pipeline");
// Adding endpoints to "DeveloperTools" group
addEndpointToGroup("DeveloperTools", "show-javascript");
// Adding endpoints to "DeveloperDocs" group (fake endpoints for link-only tools)
addEndpointToGroup("DeveloperDocs", "dev-api-docs");
addEndpointToGroup("DeveloperDocs", "dev-folder-scanning-docs");
addEndpointToGroup("DeveloperDocs", "dev-sso-guide-docs");
addEndpointToGroup("DeveloperDocs", "dev-airgapped-docs");
// CLI
addEndpointToGroup("CLI", "compress-pdf");
@@ -436,8 +451,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 +460,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");
@@ -457,9 +473,9 @@ public class EndpointConfiguration {
addEndpointToGroup("Java", "auto-rename");
addEndpointToGroup("Java", "auto-split-pdf");
addEndpointToGroup("Java", "sanitize-pdf");
addEndpointToGroup("Java", "timestamp-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,9 +485,10 @@ 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");
addEndpointToGroup("Java", "add-attachments");
addEndpointToGroup("Java", "compress-pdf");
@@ -479,15 +496,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 +524,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 +543,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 +573,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() {
@@ -576,6 +608,12 @@ public class EndpointConfiguration {
return endpointGroups.getOrDefault(group, new HashSet<>());
}
public Set<String> getAllEndpoints() {
return endpointGroups.values().stream()
.flatMap(Set::stream)
.collect(java.util.stream.Collectors.toSet());
}
private boolean isToolGroup(String group) {
return "qpdf".equals(group)
|| "OCRmyPDF".equals(group)
@@ -609,8 +647,9 @@ public class EndpointConfiguration {
}
// Check if endpoint belongs to any disabled functional group
for (String group : endpointGroups.keySet()) {
if (disabledGroups.contains(group) && endpointGroups.get(group).contains(endpoint)) {
for (Map.Entry<String, Set<String>> entry : endpointGroups.entrySet()) {
String group = entry.getKey();
if (disabledGroups.contains(group) && entry.getValue().contains(endpoint)) {
if (!isToolGroup(group)) {
return false;
}
@@ -2,12 +2,14 @@ package stirling.software.common.aop;
import java.io.IOException;
import java.time.Duration;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.MDC;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
@@ -26,7 +28,7 @@ import stirling.software.common.service.JobExecutorService;
@Component
@RequiredArgsConstructor
@Slf4j
@Order(0) // Highest precedence - executes before audit aspects
@Order(20) // Lower precedence - executes AFTER audit aspects populate MDC
public class AutoJobAspect {
private static final Duration RETRY_BASE_DELAY = Duration.ofMillis(100);
@@ -70,26 +72,29 @@ public class AutoJobAspect {
// No retries needed, simple execution
return jobExecutorService.runJobGeneric(
async,
() -> {
try {
// Note: Progress tracking is handled in TaskManager/JobExecutorService
// The trackProgress flag controls whether detailed progress is stored
// for REST API queries, not WebSocket notifications
return joinPoint.proceed(args);
} catch (Throwable ex) {
log.error(
"AutoJobAspect caught exception during job execution: {}",
ex.getMessage(),
ex);
// Rethrow RuntimeException as-is to preserve exception type
if (ex instanceof RuntimeException) {
throw (RuntimeException) ex;
}
// Wrap checked exceptions - GlobalExceptionHandler will unwrap
// BaseAppException
throw new RuntimeException(ex);
}
},
wrapWithMDC(
() -> {
try {
// Note: Progress tracking is handled in
// TaskManager/JobExecutorService
// The trackProgress flag controls whether detailed progress is
// stored
// for REST API queries, not WebSocket notifications
return joinPoint.proceed(args);
} catch (Throwable ex) {
log.error(
"AutoJobAspect caught exception during job execution: {}",
ex.getMessage(),
ex);
// Rethrow RuntimeException as-is to preserve exception type
if (ex instanceof RuntimeException) {
throw (RuntimeException) ex;
}
// Wrap checked exceptions - GlobalExceptionHandler will unwrap
// BaseAppException
throw new RuntimeException(ex);
}
}),
timeout,
queueable,
resourceWeight);
@@ -123,114 +128,108 @@ public class AutoJobAspect {
return jobExecutorService.runJobGeneric(
async,
() -> {
// Use iterative approach instead of recursion to avoid stack overflow
Throwable lastException = null;
wrapWithMDC(
() -> {
// Use iterative approach instead of recursion to avoid stack overflow
Throwable lastException = null;
// Attempt counter starts at 1 for first try
for (int currentAttempt = 1; currentAttempt <= maxRetries; currentAttempt++) {
try {
if (trackProgress && async) {
// Get jobId for progress tracking in TaskManager
// This enables REST API progress queries, not WebSocket
if (jobIdRef.get() == null) {
jobIdRef.set(getJobIdFromContext());
}
String jobId = jobIdRef.get();
if (jobId != null) {
log.debug(
"Tracking progress for job {} (attempt {}/{})",
jobId,
// Attempt counter starts at 1 for first try
for (int currentAttempt = 1;
currentAttempt <= maxRetries;
currentAttempt++) {
try {
if (trackProgress && async) {
// Get jobId for progress tracking in TaskManager
// This enables REST API progress queries, not WebSocket
if (jobIdRef.get() == null) {
jobIdRef.set(getJobIdFromContext());
}
String jobId = jobIdRef.get();
if (jobId != null) {
log.debug(
"Tracking progress for job {} (attempt {}/{})",
jobId,
currentAttempt,
maxRetries);
// Progress is tracked in TaskManager for REST API
// access
// No WebSocket notifications sent here
}
}
// Attempt to execute the operation
return joinPoint.proceed(args);
} catch (Throwable ex) {
lastException = ex;
log.error(
"AutoJobAspect caught exception during job execution (attempt"
+ " {}/{}): {}",
currentAttempt,
maxRetries);
// Progress is tracked in TaskManager for REST API access
// No WebSocket notifications sent here
}
}
maxRetries,
ex.getMessage(),
ex);
// Attempt to execute the operation
return joinPoint.proceed(args);
// Check if we should retry
if (currentAttempt < maxRetries) {
log.info(
"Retrying operation, attempt {}/{}",
currentAttempt + 1,
maxRetries);
} catch (Throwable ex) {
lastException = ex;
log.error(
"AutoJobAspect caught exception during job execution (attempt"
+ " {}/{}): {}",
currentAttempt,
maxRetries,
ex.getMessage(),
ex);
if (trackProgress && async) {
String jobId = jobIdRef.get();
if (jobId != null) {
log.debug(
"Recording retry attempt for job {} in TaskManager",
jobId);
// Retry info is tracked in TaskManager for REST API
// access
}
}
// Check if we should retry
if (currentAttempt < maxRetries) {
log.info(
"Retrying operation, attempt {}/{}",
currentAttempt + 1,
maxRetries);
// Use sleep for retry delay
// For sync jobs, both sleep and async are blocking at this
// point
// For async jobs, the delay occurs in the executor thread
long delayMs = RETRY_BASE_DELAY.toMillis() * currentAttempt;
if (trackProgress && async) {
String jobId = jobIdRef.get();
if (jobId != null) {
log.debug(
"Recording retry attempt for job {} in TaskManager",
jobId);
// Retry info is tracked in TaskManager for REST API access
try {
Thread.sleep(delayMs);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.debug(
"Retry delay interrupted for attempt {}/{}",
currentAttempt,
maxRetries);
break;
}
} else {
// No more retries, we'll throw the exception after the loop
break;
}
}
// Use non-blocking delay for all retry attempts to avoid blocking
// threads
// For sync jobs this avoids starving the tomcat thread pool under
// load
long delayMs = RETRY_BASE_DELAY.toMillis() * currentAttempt;
// Execute the retry after a delay through the JobExecutorService
// rather than blocking the current thread with sleep
CompletableFuture<Object> delayedRetry = new CompletableFuture<>();
// Use a delayed executor for non-blocking delay
CompletableFuture.delayedExecutor(delayMs, TimeUnit.MILLISECONDS)
.execute(
() -> {
// Continue the retry loop in the next iteration
// We can't return from here directly since
// we're in a Runnable
delayedRetry.complete(null);
});
// Wait for the delay to complete before continuing
try {
delayedRetry.join();
} catch (Exception e) {
Thread.currentThread().interrupt();
break;
}
} else {
// No more retries, we'll throw the exception after the loop
break;
}
}
}
// If we get here, all retries failed
if (lastException != null) {
// Rethrow RuntimeException as-is to preserve exception type
if (lastException instanceof RuntimeException) {
throw (RuntimeException) lastException;
}
// Wrap checked exceptions - GlobalExceptionHandler will unwrap
// BaseAppException
throw new RuntimeException(
"Job failed after "
+ maxRetries
+ " attempts: "
+ lastException.getMessage(),
lastException);
}
// If we get here, all retries failed
if (lastException != null) {
// Rethrow RuntimeException as-is to preserve exception type
if (lastException instanceof RuntimeException) {
throw (RuntimeException) lastException;
}
// Wrap checked exceptions - GlobalExceptionHandler will unwrap
// BaseAppException
throw new RuntimeException(
"Job failed after "
+ maxRetries
+ " attempts: "
+ lastException.getMessage(),
lastException);
}
// This should never happen if lastException is properly tracked
throw new RuntimeException("Job failed but no exception was recorded");
},
// This should never happen if lastException is properly tracked
throw new RuntimeException("Job failed but no exception was recorded");
}),
timeout,
queueable,
resourceWeight);
@@ -299,4 +298,32 @@ public class AutoJobAspect {
return null;
}
}
/**
* Wraps a supplier to propagate MDC context to background threads. Captures MDC on request
* thread and restores it in the background thread. Ensures proper cleanup to prevent context
* leakage across jobs in thread pools.
*/
private <T> Supplier<T> wrapWithMDC(Supplier<T> supplier) {
final Map<String, String> captured = MDC.getCopyOfContextMap();
return () -> {
final Map<String, String> previous = MDC.getCopyOfContextMap();
try {
// Set the captured context (or clear if none was captured)
if (captured != null) {
MDC.setContextMap(new HashMap<>(captured));
} else {
MDC.clear();
}
return supplier.get();
} finally {
// Restore previous state (or clear if there was none)
if (previous != null) {
MDC.setContextMap(previous);
} else {
MDC.clear();
}
}
};
}
}
@@ -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,10 +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;
@@ -41,6 +45,7 @@ public class RuntimePathConfig {
// Pipeline paths
private final String pipelineWatchedFoldersPath;
private final List<String> pipelineWatchedFoldersPaths;
private final String pipelineFinishedFoldersPath;
private final String pipelineDefaultWebUiConfigs;
private final String pipelinePath;
@@ -49,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,
@@ -72,13 +84,16 @@ public class RuntimePathConfig {
defaultWebUIConfigs,
pipeline != null ? pipeline.getWebUIConfigsDir() : null);
// Validate path conflicts after all paths are resolved
validatePipelinePaths();
boolean isDocker = isRunningInDocker();
// Initialize Operation paths
String defaultWeasyPrintPath = isDocker ? "/opt/venv/bin/weasyprint" : "weasyprint";
String defaultUnoConvertPath = isDocker ? "/opt/venv/bin/unoconvert" : "unoconvert";
String defaultUnoConvertPath = isDocker ? "/usr/local/bin/unoconvert" : "unoconvert";
String defaultCalibrePath = isDocker ? "/opt/calibre/ebook-convert" : "ebook-convert";
String defaultOcrMyPdfPath = isDocker ? "/usr/bin/ocrmypdf" : "ocrmypdf";
String defaultOcrMyPdfPath = isDocker ? "/opt/venv/bin/ocrmypdf" : "ocrmypdf";
String defaultSOfficePath = isDocker ? "/usr/bin/soffice" : "soffice";
Operations operations = customPaths.getOperations();
@@ -129,6 +144,140 @@ public class RuntimePathConfig {
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"));
}
@@ -0,0 +1,23 @@
package stirling.software.common.configuration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.concurrent.SimpleAsyncTaskScheduler;
/**
* Configures the scheduler used by all {@code @Scheduled} methods. Uses virtual threads so that
* long-running scheduled tasks (e.g. cleanup, license checks, file monitoring) never block each
* other — each runs on its own lightweight virtual thread.
*/
@Configuration
public class SchedulingConfig {
@Bean
public TaskScheduler taskScheduler() {
SimpleAsyncTaskScheduler scheduler = new SimpleAsyncTaskScheduler();
scheduler.setVirtualThreads(true);
scheduler.setThreadNamePrefix("scheduled-vt-");
return scheduler;
}
}
@@ -0,0 +1,49 @@
package stirling.software.common.constants;
/**
* Centralized constants for JWT token management.
*
* <p>These defaults are used when configuration values are not explicitly set.
*/
public final class JwtConstants {
private JwtConstants() {
throw new UnsupportedOperationException("Utility class");
}
/** Default JWT access token lifetime in minutes (24 hours). */
public static final int DEFAULT_TOKEN_EXPIRY_MINUTES = 1440;
/** Default desktop client token lifetime in minutes (30 days). */
public static final int DEFAULT_DESKTOP_TOKEN_EXPIRY_MINUTES = 43200;
/**
* Default refresh grace period in minutes.
*
* <p>Allows refresh of expired tokens within this window after expiration.
*/
public static final int DEFAULT_REFRESH_GRACE_MINUTES = 15;
/**
* Default allowed clock skew in seconds.
*
* <p>Tolerates small time drift between client and server clocks during validation.
*/
public static final int DEFAULT_CLOCK_SKEW_SECONDS = 60;
/** Milliseconds per minute. */
public static final long MILLIS_PER_MINUTE = 60_000L;
/** Seconds per minute. */
public static final long SECONDS_PER_MINUTE = 60L;
/** JWT issuer identifier. */
public static final String ISSUER = "https://stirling.com";
/**
* Maximum refresh attempts allowed within the grace period window.
*
* <p>Prevents abuse of expired tokens by limiting refresh attempts.
*/
public static final int MAX_REFRESH_ATTEMPTS_IN_GRACE = 3;
}
@@ -29,6 +29,8 @@ import org.springframework.stereotype.Component;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.annotation.PostConstruct;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
@@ -37,6 +39,7 @@ import lombok.extern.slf4j.Slf4j;
import stirling.software.common.configuration.InstallationPathConfig;
import stirling.software.common.configuration.YamlPropertySourceFactory;
import stirling.software.common.constants.JwtConstants;
import stirling.software.common.model.exception.UnsupportedProviderException;
import stirling.software.common.model.oauth2.GitHubProvider;
import stirling.software.common.model.oauth2.GoogleProvider;
@@ -98,9 +101,93 @@ public class ApplicationProperties {
return propertySource;
}
/**
* Initialize fileUploadLimit from environment variables if not set in settings.yml. Supports
* SYSTEMFILEUPLOADLIMIT (format: "100MB") and SYSTEM_MAXFILESIZE (format: "100" in MB).
*/
@PostConstruct
public void initializeFileUploadLimitFromEnv() {
// Only override if fileUploadLimit is not already set in settings.yml
if (system.getFileUploadLimit() == null || system.getFileUploadLimit().isEmpty()) {
String fileUploadLimit = null;
// Check SYSTEMFILEUPLOADLIMIT first (format: "100MB", "1GB", etc.)
String systemFileUploadLimit = java.lang.System.getenv("SYSTEMFILEUPLOADLIMIT");
if (systemFileUploadLimit != null && !systemFileUploadLimit.trim().isEmpty()) {
fileUploadLimit = systemFileUploadLimit.trim();
log.info("Setting fileUploadLimit from SYSTEMFILEUPLOADLIMIT: {}", fileUploadLimit);
} else {
// Check SYSTEM_MAXFILESIZE (format: number in MB, e.g., "100")
String systemMaxFileSize = java.lang.System.getenv("SYSTEM_MAXFILESIZE");
if (systemMaxFileSize != null && !systemMaxFileSize.trim().isEmpty()) {
try {
// Validate it's a number
long sizeInMB = Long.parseLong(systemMaxFileSize.trim());
if (sizeInMB > 0 && sizeInMB <= 999) {
fileUploadLimit = sizeInMB + "MB";
log.info(
"Setting fileUploadLimit from SYSTEM_MAXFILESIZE: {}MB",
sizeInMB);
} else {
log.warn(
"SYSTEM_MAXFILESIZE value {} is out of valid range (1-999), ignoring",
sizeInMB);
}
} catch (NumberFormatException e) {
log.warn(
"SYSTEM_MAXFILESIZE value '{}' is not a valid number, ignoring",
systemMaxFileSize);
}
}
}
if (fileUploadLimit != null) {
system.setFileUploadLimit(fileUploadLimit);
}
}
}
@Data
public static class AutoPipeline {
private String outputFolder;
private FileReadiness fileReadiness = new FileReadiness();
/**
* Configuration for the {@link stirling.software.common.util.FileReadinessChecker}.
* Controls how the pipeline determines whether a file is fully written and stable before
* processing begins.
*/
@Data
public static class FileReadiness {
/**
* Master toggle. When {@code false} every readiness check is skipped and all files are
* considered immediately ready (preserves legacy behaviour).
*/
private boolean enabled = true;
/**
* How long (in milliseconds) a file must remain unmodified before it is considered
* stable. Files modified more recently than this threshold are skipped and retried on
* the next scan cycle. Default: 5 000 ms (5 seconds).
*/
private long settleTimeMillis = 5000;
/**
* How long (in milliseconds) to pause between two consecutive file-size reads when
* checking whether a file is still being written. If the size differs between the two
* reads the file is considered unstable. This catches active copies on Linux/macOS
* where advisory locking alone cannot detect a mid-copy file. Default: 500 ms.
*/
private long sizeCheckDelayMillis = 500;
/**
* Optional list of file extensions (without the leading dot, case-insensitive) that are
* allowed through the readiness check. An empty list means all extensions are accepted.
* Example: {@code ["pdf", "tiff"]} will skip any file whose extension is not {@code
* pdf} or {@code tiff}.
*/
private List<String> allowedExtensions = new java.util.ArrayList<>();
}
}
@Data
@@ -164,6 +251,8 @@ public class ApplicationProperties {
private String customGlobalAPIKey;
private Jwt jwt = new Jwt();
private Validation validation = new Validation();
private Timestamp timestamp = new Timestamp();
private String xFrameOptions = "DENY";
public Boolean isAltLogin() {
return saml2.getEnabled() || oauth2.getEnabled();
@@ -315,11 +404,11 @@ public class ApplicationProperties {
}
public boolean isSettingsValid() {
return !ValidationUtils.isStringEmpty(this.getIssuer())
&& !ValidationUtils.isStringEmpty(this.getClientId())
&& !ValidationUtils.isStringEmpty(this.getClientSecret())
&& !ValidationUtils.isCollectionEmpty(this.getScopes())
&& !ValidationUtils.isStringEmpty(this.getUseAsUsername());
return !ValidationUtils.isStringEmpty(this.issuer)
&& !ValidationUtils.isStringEmpty(this.clientId)
&& !ValidationUtils.isStringEmpty(this.clientSecret)
&& !ValidationUtils.isCollectionEmpty(this.scopes)
&& !ValidationUtils.isStringEmpty(this.useAsUsername);
}
@Data
@@ -344,12 +433,107 @@ public class ApplicationProperties {
}
}
/**
* JWT token configuration.
*
* <p><b>BREAKING CHANGE (v2.0):</b> Default token expiry increased from 12 hours (720
* minutes) to 24 hours (1440 minutes). If you require the previous behavior, explicitly set
* {@code tokenExpiryMinutes: 720} in your configuration.
*/
@Data
public static class Jwt {
private boolean enableKeystore = true;
private boolean enableKeyRotation = false;
private boolean enableKeyCleanup = true;
private int keyRetentionDays = 7;
/**
* JWT access token lifetime in minutes for web clients.
*
* <p>Default: {@value JwtConstants#DEFAULT_TOKEN_EXPIRY_MINUTES} minutes (24 hours).
*
* <p><b>BREAKING CHANGE:</b> Previously hardcoded to 720 minutes (12 hours). Now
* defaults to 1440 minutes (24 hours).
*/
private int tokenExpiryMinutes = JwtConstants.DEFAULT_TOKEN_EXPIRY_MINUTES;
/**
* JWT access token lifetime in minutes for desktop clients (Tauri app).
*
* <p>Desktop clients are automatically detected via User-Agent header and receive
* longer-lived tokens because they run on personal devices with OS-level encrypted
* storage (macOS Keychain, Windows Credential Manager, Linux Secret Service).
*
* <p>This provides better UX (login once per month) while maintaining security through
* device encryption and secure storage, matching the behavior of popular desktop apps
* like Slack, Discord, VS Code, etc.
*
* <p>Default: 43200 minutes (30 days).
*/
private int desktopTokenExpiryMinutes = 43200;
/**
* Allowed clock skew in seconds for JWT validation.
*
* <p>Tolerates small time drift between client and server clocks. Tokens that are
* slightly expired or slightly in the future (within this window) will still be
* accepted.
*
* <p>Default: {@value JwtConstants#DEFAULT_CLOCK_SKEW_SECONDS} seconds.
*/
private int allowedClockSkewSeconds = JwtConstants.DEFAULT_CLOCK_SKEW_SECONDS;
/**
* Grace period in minutes for refreshing expired tokens.
*
* <p>Allows token refresh using an expired access token if the token expired within
* this many minutes. This provides better UX by allowing users to refresh slightly
* expired tokens without re-authentication.
*
* <p>Rate limiting is applied to prevent abuse of expired tokens within the grace
* window (max {@value JwtConstants#MAX_REFRESH_ATTEMPTS_IN_GRACE} attempts).
*
* <p>Default: {@value JwtConstants#DEFAULT_REFRESH_GRACE_MINUTES} minutes.
*/
private int refreshGraceMinutes = JwtConstants.DEFAULT_REFRESH_GRACE_MINUTES;
/**
* Calculate number of days to retain old JWT signing keys.
*
* <p>Automatically calculated based on the longest token lifetime plus a proportional
* safety buffer. Keys must be retained for at least as long as the tokens they signed
* remain valid, otherwise token verification will fail.
*
* <p>Formula: ceil((maxTokenExpiry + 10% buffer + refreshGrace + clockSkew) / 1440)
*
* <p>The buffer includes:
*
* <ul>
* <li>10% of token lifetime (scales with token duration)
* <li>Token refresh grace period ({@link #refreshGraceMinutes})
* <li>Clock skew tolerance ({@link #allowedClockSkewSeconds} converted to minutes)
* </ul>
*
* @return calculated key retention period in days
*/
public int getKeyRetentionDays() {
final int MINUTES_PER_DAY = 1440;
final double BUFFER_PERCENTAGE = 0.10; // 10% buffer
int maxTokenExpiryMinutes = Math.max(tokenExpiryMinutes, desktopTokenExpiryMinutes);
// Add 10% buffer (scales with token lifetime)
int bufferMinutes = (int) Math.ceil(maxTokenExpiryMinutes * BUFFER_PERCENTAGE);
// Add refresh grace period
bufferMinutes += refreshGraceMinutes;
// Add clock skew (convert seconds to minutes, round up)
bufferMinutes += (int) Math.ceil(allowedClockSkewSeconds / 60.0);
// Total retention in minutes, convert to days (round up)
int totalMinutes = maxTokenExpiryMinutes + bufferMinutes;
return (int) Math.ceil(totalMinutes / (double) MINUTES_PER_DAY);
}
}
@Data
@@ -386,6 +570,12 @@ public class ApplicationProperties {
private boolean hardFail = false;
}
}
@Data
public static class Timestamp {
private String defaultTsaUrl = "http://timestamp.digicert.com";
private List<String> customTsaUrls = new ArrayList<>();
}
}
@Data
@@ -430,19 +620,17 @@ public class ApplicationProperties {
}
public boolean isAnalyticsEnabled() {
return this.getEnableAnalytics() != null && this.getEnableAnalytics();
return this.enableAnalytics != null && this.enableAnalytics;
}
public boolean isPosthogEnabled() {
// Treat null as enabled when analytics is enabled
return this.isAnalyticsEnabled()
&& (this.getEnablePosthog() == null || this.getEnablePosthog());
return this.isAnalyticsEnabled() && (this.enablePosthog == null || this.enablePosthog);
}
public boolean isScarfEnabled() {
// Treat null as enabled when analytics is enabled
return this.isAnalyticsEnabled()
&& (this.getEnableScarf() == null || this.getEnableScarf());
return this.isAnalyticsEnabled() && (this.enableScarf == null || this.enableScarf);
}
}
@@ -458,7 +646,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;
}
@@ -565,6 +755,9 @@ public class ApplicationProperties {
private String appNameNavbar;
private List<String> languages;
private String logoStyle = "classic"; // Options: "classic" (default) or "modern"
private boolean defaultHideUnavailableTools = false;
private boolean defaultHideUnavailableConversions = false;
private HideDisabledTools hideDisabledTools = new HideDisabledTools();
public String getAppNameNavbar() {
return appNameNavbar != null && !appNameNavbar.trim().isEmpty() ? appNameNavbar : null;
@@ -577,6 +770,12 @@ public class ApplicationProperties {
}
return "classic"; // default
}
@Data
public static class HideDisabledTools {
private boolean googleDrive = false;
private boolean mobileQRScanner = false;
}
}
@Data
@@ -764,6 +963,15 @@ public class ApplicationProperties {
private boolean ssoAutoLogin;
private boolean database;
private CustomMetadata customMetadata = new CustomMetadata();
private GoogleDrive googleDrive = new GoogleDrive();
@Data
public static class GoogleDrive {
private boolean enabled = false;
private String clientId = "";
private String apiKey = "";
private String appId = "";
}
@Data
public static class CustomMetadata {
@@ -813,6 +1021,12 @@ public class ApplicationProperties {
private boolean enabled = true;
private int level = 2; // 0=OFF, 1=BASIC, 2=STANDARD, 3=VERBOSE
private int retentionDays = 90;
private boolean captureFileHash =
false; // Capture SHA-256 hash of files (increases processing time)
private boolean capturePdfAuthor =
false; // Capture PDF author metadata (increases processing time)
private boolean captureOperationResults =
false; // Capture operation return values (not recommended, high volume)
}
@Data
@@ -0,0 +1,98 @@
package stirling.software.common.model;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonInclude;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/** Form field information with coordinates for interactive form viewer. */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
@Schema(description = "Form field with coordinates and metadata")
public class FormFieldWithCoordinates {
@Schema(description = "Fully qualified field name", example = "form1.firstName")
private String name;
@Schema(description = "Display label for the field", example = "First Name")
private String label;
@Schema(description = "Field type: text, checkbox, radio, combobox, listbox, button, signature")
private String type;
@Schema(description = "Current field value")
private String value;
@Schema(
description =
"Available options (export values) for choice fields"
+ " (dropdown, radio, listbox)")
private List<String> options;
@Schema(
description =
"Human-readable display labels for choice field options,"
+ " parallel to the 'options' list. Null when identical to options.")
private List<String> displayOptions;
@Schema(description = "Whether the field is required")
private boolean required;
@Schema(description = "Whether the field is read-only")
private boolean readOnly;
@Schema(description = "Whether this is a multi-select list box")
private boolean multiSelect;
@Schema(description = "Whether this is a multi-line text field")
private boolean multiline;
@Schema(description = "Tooltip/alternate name for the field")
private String tooltip;
@Schema(description = "Widget coordinates on each page (fields can have multiple widgets)")
private List<WidgetCoordinates> widgets;
/**
* Coordinates for a single widget annotation (visual representation of the field). A field can
* have multiple widgets if it appears on multiple pages.
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
@Schema(description = "Widget coordinates in PDF space")
public static class WidgetCoordinates {
@Schema(description = "Page index (0-based)", example = "0")
private int pageIndex;
@Schema(description = "X coordinate in PDF points (lower-left origin)")
private float x;
@Schema(description = "Y coordinate in PDF points (lower-left origin)")
private float y;
@Schema(description = "Width in PDF points")
private float width;
@Schema(description = "Height in PDF points")
private float height;
@Schema(description = "Export value for this widget (radio/checkbox buttons only)")
private String exportValue;
@Schema(description = "Font size in PDF points")
private Float fontSize;
}
}
@@ -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;
}
}
*/
@@ -42,7 +42,7 @@ public enum Role {
// Using the fromString method to get the Role enum based on the roleId
Role role = fromString(roleId);
// Return the roleName of the found Role enum
return role.getRoleName();
return role.roleName;
}
// Method to retrieve all role IDs and role names
@@ -50,14 +50,14 @@ public enum Role {
// Using LinkedHashMap to preserve order
Map<String, String> roleDetails = new LinkedHashMap<>();
for (Role role : Role.values()) {
roleDetails.put(role.getRoleId(), role.getRoleName());
roleDetails.put(role.roleId, role.roleName);
}
return roleDetails;
}
public static Role fromString(String roleId) {
for (Role role : Role.values()) {
if (role.getRoleId().equalsIgnoreCase(roleId)) {
if (role.roleId.equalsIgnoreCase(roleId)) {
return role;
}
}
@@ -117,13 +117,13 @@ public class Provider {
+ ", clientName="
+ getClientName()
+ ", clientId="
+ getClientId()
+ clientId
+ ", clientSecret="
+ (getClientSecret() != null && !getClientSecret().isEmpty() ? "*****" : "NULL")
+ (clientSecret != null && !clientSecret.isEmpty() ? "*****" : "NULL")
+ ", scopes="
+ getScopes()
+ ", useAsUsername="
+ getUseAsUsername()
+ useAsUsername
+ "]";
}
}
@@ -1,6 +1,8 @@
package stirling.software.common.service;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.UUID;
@@ -21,6 +23,9 @@ import lombok.extern.slf4j.Slf4j;
@Slf4j
public class FileStorage {
/** Holds the result of a stream-to-disk store operation: the file ID and the bytes written. */
public record StoredFile(String fileId, long size) {}
@Value("${stirling.tempDir:/tmp/stirling-files}")
private String tempDirPath;
@@ -104,6 +109,40 @@ public class FileStorage {
return Files.readAllBytes(filePath);
}
/**
* Retrieve a file by its ID as a streaming InputStream. The caller is responsible for closing
* the returned stream.
*
* @param fileId The ID of the file to retrieve
* @return A buffered InputStream for the file
* @throws IOException If the file doesn't exist or can't be read
*/
public InputStream retrieveInputStream(String fileId) throws IOException {
Path filePath = getFilePath(fileId);
// Let Files.newInputStream throw NoSuchFileException naturally — avoids TOCTOU race
// between exists-check and open when another thread may delete concurrently.
return new BufferedInputStream(Files.newInputStream(filePath));
}
/**
* Store data from an InputStream as a file and return its unique ID and byte count. Streams
* directly to disk without buffering the entire content in heap.
*
* @param inputStream The input stream to read from
* @param originalName The original name of the file (unused, kept for API symmetry)
* @return A {@link StoredFile} containing the file ID and the number of bytes written
* @throws IOException If there is an error storing the file
*/
public StoredFile storeInputStream(InputStream inputStream, String originalName)
throws IOException {
String fileId = generateFileId();
Path filePath = getFilePath(fileId);
Files.createDirectories(filePath.getParent());
long size = Files.copy(inputStream, filePath);
log.debug("Stored input stream with ID: {}", fileId);
return new StoredFile(fileId, size);
}
/**
* Delete a file by its ID
*
@@ -35,7 +35,7 @@ public class JobExecutorService {
private final HttpServletRequest request;
private final ResourceMonitor resourceMonitor;
private final JobQueue jobQueue;
private final ExecutorService executor = ExecutorFactory.newVirtualOrCachedThreadExecutor();
private final ExecutorService executor = ExecutorFactory.newVirtualThreadExecutor();
private final long effectiveTimeoutMs;
@Autowired(required = false)
@@ -254,10 +254,13 @@ public class JobExecutorService {
return ResponseEntity.internalServerError()
.body(Map.of("error", "Job timed out after " + timeoutToUse + " ms"));
} catch (RuntimeException e) {
// Check if this is a wrapped typed exception that should be handled by
// GlobalExceptionHandler
// Check if this is a typed exception that should be handled by
// GlobalExceptionHandler (either directly or wrapped)
Throwable cause = e.getCause();
if (cause instanceof stirling.software.common.util.ExceptionUtils.BaseAppException
if (e instanceof IllegalArgumentException
|| cause
instanceof
stirling.software.common.util.ExceptionUtils.BaseAppException
|| cause
instanceof
stirling.software.common.util.ExceptionUtils
@@ -313,7 +316,7 @@ public class JobExecutorService {
filename =
disposition.substring(
disposition.indexOf("filename=") + 9,
disposition.lastIndexOf("\""));
disposition.lastIndexOf('"'));
}
}
@@ -44,8 +44,10 @@ public class JobQueue implements SmartLifecycle {
private volatile BlockingQueue<QueuedJob> jobQueue;
private final Map<String, QueuedJob> jobMap = new ConcurrentHashMap<>();
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
private final ExecutorService jobExecutor = ExecutorFactory.newVirtualOrCachedThreadExecutor();
private final ScheduledExecutorService scheduler =
Executors.newSingleThreadScheduledExecutor(
Thread.ofVirtual().name("job-queue-scheduler-", 0).factory());
private final ExecutorService jobExecutor = ExecutorFactory.newVirtualThreadExecutor();
private final Object queueLock = new Object(); // Lock for synchronizing queue operations
private boolean shuttingDown = false;
@@ -0,0 +1,29 @@
package stirling.software.common.service;
/**
* Interface for checking license status dynamically. Implementation provided by proprietary module
* when available.
*/
public interface LicenseServiceInterface {
/**
* Get the license type as a string.
*
* @return "NORMAL", "SERVER", or "ENTERPRISE"
*/
String getLicenseTypeName();
/**
* Check if running Pro or higher (SERVER or ENTERPRISE license).
*
* @return true if SERVER or ENTERPRISE license is active
*/
boolean isRunningProOrHigher();
/**
* Check if running Enterprise edition.
*
* @return true if ENTERPRISE license is active
*/
boolean isRunningEE();
}
@@ -9,6 +9,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Pattern;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
@@ -25,6 +26,9 @@ import lombok.extern.slf4j.Slf4j;
public class MobileScannerService {
private static final long SESSION_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
private static final Pattern FILENAME_SANITIZE_PATTERN = Pattern.compile("[^a-zA-Z0-9._-]");
private static final Pattern SESSION_ID_VALIDATION_PATTERN = Pattern.compile("[a-zA-Z0-9-]+");
private static final Pattern FILE_EXTENSION_PATTERN = Pattern.compile("[.][^.]+$");
private final Map<String, SessionData> activeSessions = new ConcurrentHashMap<>();
private final Path tempDirectory;
@@ -121,10 +125,11 @@ public class MobileScannerService {
// Handle duplicate filenames
int counter = 1;
while (Files.exists(filePath)) {
String nameWithoutExt = safeFilename.replaceFirst("[.][^.]+$", "");
String nameWithoutExt =
FILE_EXTENSION_PATTERN.matcher(safeFilename).replaceFirst("");
String ext =
safeFilename.contains(".")
? safeFilename.substring(safeFilename.lastIndexOf("."))
? safeFilename.substring(safeFilename.lastIndexOf('.'))
: "";
safeFilename = nameWithoutExt + "-" + counter + ext;
filePath = sessionDir.resolve(safeFilename).normalize().toAbsolutePath();
@@ -271,14 +276,14 @@ public class MobileScannerService {
throw new IllegalArgumentException("Session ID cannot be empty");
}
// Basic validation: alphanumeric and hyphens only
if (!sessionId.matches("[a-zA-Z0-9-]+")) {
if (!SESSION_ID_VALIDATION_PATTERN.matcher(sessionId).matches()) {
throw new IllegalArgumentException("Invalid session ID format");
}
}
private String sanitizeFilename(String filename) {
// Remove path traversal attempts and dangerous characters
String sanitized = filename.replaceAll("[^a-zA-Z0-9._-]", "_");
String sanitized = FILENAME_SANITIZE_PATTERN.matcher(filename).replaceAll("_");
// Ensure we have a non-empty, safe filename
if (sanitized.isBlank()) {
sanitized = "upload-" + System.currentTimeMillis();
@@ -169,7 +169,10 @@ public class PdfMetadataService {
.getAuthor();
if (userService != null) {
author = author.replace("username", userService.getCurrentUsername());
String username = userService.getCurrentUsername();
if (username != null) {
author = author.replace("username", username);
}
}
}
pdf.getDocumentInformation().setAuthor(author);
@@ -43,7 +43,9 @@ public class ResourceMonitor {
@Value("${stirling.resource.monitor.interval-ms:60000}")
private long monitorIntervalMs = 60000; // 60 seconds
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
private final ScheduledExecutorService scheduler =
Executors.newSingleThreadScheduledExecutor(
Thread.ofVirtual().name("resource-monitor-", 0).factory());
private final MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean();
private final OperatingSystemMXBean osMXBean = ManagementFactory.getOperatingSystemMXBean();
@@ -226,10 +226,11 @@ public class SsrfProtectionService {
}
private boolean isPrivateIPv4Range(String ip) {
// Includes RFC1918, loopback, link-local, and unspecified addresses
// Includes RFC1918, RFC6598, loopback, link-local, and unspecified addresses
return ip.startsWith("10.")
|| ip.startsWith("192.168.")
|| (ip.startsWith("172.") && isInRange172(ip))
|| (ip.startsWith("100.") && isInRange100(ip))
|| ip.startsWith("169.254.")
|| ip.startsWith("127.")
|| "0.0.0.0".equals(ip);
@@ -247,6 +248,18 @@ public class SsrfProtectionService {
return false;
}
private boolean isInRange100(String ip) {
String[] parts = ip.split("\\.");
if (parts.length >= 2) {
try {
int secondOctet = Integer.parseInt(parts[1]);
return secondOctet >= 64 && secondOctet <= 127;
} catch (NumberFormatException e) {
}
}
return false;
}
private boolean isCloudMetadataAddress(String ip) {
String normalizedIp = normalizeIpv4MappedAddress(ip);
// Cloud metadata endpoints for AWS, GCP, Azure, Oracle Cloud, and IBM Cloud
@@ -1,8 +1,8 @@
package stirling.software.common.service;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
@@ -19,7 +19,6 @@ import java.util.zip.ZipInputStream;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import io.github.pixee.security.ZipSecurity;
@@ -42,7 +41,8 @@ public class TaskManager {
private final FileStorage fileStorage;
private final ScheduledExecutorService cleanupExecutor =
Executors.newSingleThreadScheduledExecutor();
Executors.newSingleThreadScheduledExecutor(
Thread.ofVirtual().name("task-cleanup-", 0).factory());
/** Initialize the task manager and start the cleanup scheduler */
public TaskManager(FileStorage fileStorage) {
@@ -362,39 +362,29 @@ public class TaskManager {
String zipFileId, String originalZipFileName) throws IOException {
List<ResultFile> extractedFiles = new ArrayList<>();
MultipartFile zipFile = fileStorage.retrieveFile(zipFileId);
try (ZipInputStream zipIn =
ZipSecurity.createHardenedInputStream(
new ByteArrayInputStream(zipFile.getBytes()))) {
try (InputStream fileStream = fileStorage.retrieveInputStream(zipFileId);
ZipInputStream zipIn =
ZipSecurity.createHardenedInputStream(
new BufferedInputStream(fileStream))) {
ZipEntry entry;
while ((entry = zipIn.getNextEntry()) != null) {
if (!entry.isDirectory()) {
// Use buffered reading for memory safety
ByteArrayOutputStream out = new ByteArrayOutputStream();
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = zipIn.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
}
byte[] fileContent = out.toByteArray();
String contentType = determineContentType(entry.getName());
String individualFileId = fileStorage.storeBytes(fileContent, entry.getName());
// storeInputStream returns the fileId and byte count — no extra stat needed
FileStorage.StoredFile stored =
fileStorage.storeInputStream(zipIn, entry.getName());
ResultFile resultFile =
ResultFile.builder()
.fileId(individualFileId)
.fileId(stored.fileId())
.fileName(entry.getName())
.contentType(contentType)
.fileSize(fileContent.length)
.fileSize(stored.size())
.build();
extractedFiles.add(resultFile);
log.debug(
"Extracted file: {} (size: {} bytes)",
entry.getName(),
fileContent.length);
"Extracted file: {} (size: {} bytes)", entry.getName(), stored.size());
}
zipIn.closeEntry();
}
@@ -466,4 +456,24 @@ public class TaskManager {
}
return null;
}
/**
* Find the job key that owns a given file ID.
*
* @param fileId file identifier to look up
* @return scoped job key if found, otherwise null
*/
public String findJobKeyByFileId(String fileId) {
for (Map.Entry<String, JobResult> entry : jobResults.entrySet()) {
JobResult jobResult = entry.getValue();
if (jobResult.hasFiles()) {
for (ResultFile resultFile : jobResult.getAllResultFiles()) {
if (fileId.equals(resultFile.getFileId())) {
return entry.getKey();
}
}
}
}
return null;
}
}
@@ -4,6 +4,7 @@ import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Enumeration;
@@ -30,15 +31,7 @@ import stirling.software.common.service.CustomPDFDocumentFactory;
@UtilityClass
public class CbzUtils {
public byte[] convertCbzToPdf(
MultipartFile cbzFile,
CustomPDFDocumentFactory pdfDocumentFactory,
TempFileManager tempFileManager)
throws IOException {
return convertCbzToPdf(cbzFile, pdfDocumentFactory, tempFileManager, false);
}
public byte[] convertCbzToPdf(
public TempFile convertCbzToPdf(
MultipartFile cbzFile,
CustomPDFDocumentFactory pdfDocumentFactory,
TempFileManager tempFileManager,
@@ -64,70 +57,90 @@ public class CbzUtils {
try (PDDocument document = pdfDocumentFactory.createNewDocument();
ZipFile zipFile = new ZipFile(tempFile.getFile())) {
// Pass 1: collect sorted image names (cheap just strings, no image data)
List<String> sortedImageNames = new ArrayList<>();
Enumeration<? extends ZipEntry> entries = zipFile.entries();
List<ImageEntryData> imageEntries = new ArrayList<>();
while (entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
if (!entry.isDirectory() && isImageFile(entry.getName())) {
try (InputStream is = zipFile.getInputStream(entry)) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
is.transferTo(baos);
imageEntries.add(
new ImageEntryData(entry.getName(), baos.toByteArray()));
} catch (IOException e) {
log.warn("Error reading image {}: {}", entry.getName(), e.getMessage());
}
sortedImageNames.add(entry.getName());
}
}
sortedImageNames.sort(new NaturalOrderComparator());
imageEntries.sort(
Comparator.comparing(ImageEntryData::name, new NaturalOrderComparator()));
if (imageEntries.isEmpty()) {
if (sortedImageNames.isEmpty()) {
throw ExceptionUtils.createCbzNoImagesException();
}
for (ImageEntryData imageEntry : imageEntries) {
try {
PDImageXObject pdImage =
PDImageXObject.createFromByteArray(
document, imageEntry.data(), imageEntry.name());
PDPage page =
new PDPage(
new PDRectangle(pdImage.getWidth(), pdImage.getHeight()));
document.addPage(page);
try (PDPageContentStream contentStream =
new PDPageContentStream(
document,
page,
PDPageContentStream.AppendMode.OVERWRITE,
true,
true)) {
contentStream.drawImage(pdImage, 0, 0);
// Pass 2: load ONE image at a time peak memory = max(single image)
for (String imageName : sortedImageNames) {
ZipEntry entry = zipFile.getEntry(imageName);
try (InputStream is = zipFile.getInputStream(entry)) {
ByteArrayOutputStream imgBaos = new ByteArrayOutputStream();
is.transferTo(imgBaos);
byte[] imageBytes = imgBaos.toByteArray();
try {
PDImageXObject pdImage =
PDImageXObject.createFromByteArray(
document, imageBytes, imageName);
PDPage page =
new PDPage(
new PDRectangle(
pdImage.getWidth(), pdImage.getHeight()));
document.addPage(page);
try (PDPageContentStream contentStream =
new PDPageContentStream(
document,
page,
PDPageContentStream.AppendMode.OVERWRITE,
true,
true)) {
contentStream.drawImage(pdImage, 0, 0);
}
} catch (IOException e) {
log.warn("Error processing image {}: {}", imageName, e.getMessage());
}
// imageBytes eligible for GC after each iteration
} catch (IOException e) {
log.warn(
"Error processing image {}: {}", imageEntry.name(), e.getMessage());
log.warn("Error reading image {}: {}", imageName, e.getMessage());
}
}
if (document.getNumberOfPages() == 0) {
throw ExceptionUtils.createCbzCorruptedImagesException();
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
document.save(baos);
byte[] pdfBytes = baos.toByteArray();
// Apply Ghostscript optimization if requested
if (optimizeForEbook) {
try {
return GeneralUtils.optimizePdfWithGhostscript(pdfBytes);
} catch (IOException e) {
log.warn("Ghostscript optimization failed, returning unoptimized PDF", e);
// Write to TempFile (not BAOS)
TempFile pdfTempFile = new TempFile(tempFileManager, ".pdf");
try {
document.save(pdfTempFile.getFile());
if (optimizeForEbook) {
try {
byte[] pdfBytes = Files.readAllBytes(pdfTempFile.getPath());
byte[] optimized = GeneralUtils.optimizePdfWithGhostscript(pdfBytes);
pdfTempFile.close();
TempFile optimizedFile = new TempFile(tempFileManager, ".pdf");
try {
Files.write(optimizedFile.getPath(), optimized);
return optimizedFile;
} catch (Exception e) {
optimizedFile.close();
throw e;
}
} catch (IOException e) {
log.warn(
"Ghostscript optimization failed, returning unoptimized PDF",
e);
}
}
}
return pdfBytes;
return pdfTempFile;
} catch (Exception e) {
pdfTempFile.close();
throw e;
}
}
}
}
@@ -175,8 +188,6 @@ public class CbzUtils {
return RegexPatternUtils.getInstance().getImageFilePattern().matcher(filename).matches();
}
private record ImageEntryData(String name, byte[] data) {}
private class NaturalOrderComparator implements Comparator<String> {
@Override
public int compare(String s1, String s2) {
@@ -13,11 +13,18 @@ public class EmlToPdf {
public static String convertEmlToHtml(byte[] emlBytes, EmlToPdfRequest request)
throws IOException {
return convertEmlToHtml(emlBytes, request, null);
}
public static String convertEmlToHtml(
byte[] emlBytes, EmlToPdfRequest request, CustomHtmlSanitizer customHtmlSanitizer)
throws IOException {
EmlProcessingUtils.validateEmlInput(emlBytes);
EmlParser.EmailContent emailContent =
EmlParser.extractEmailContent(emlBytes, request, null);
return EmlProcessingUtils.generateEnhancedEmailHtml(emailContent, request, null);
EmlParser.extractEmailContent(emlBytes, request, customHtmlSanitizer);
return EmlProcessingUtils.generateEnhancedEmailHtml(
emailContent, request, customHtmlSanitizer);
}
public static byte[] convertEmlToPdf(
@@ -2,30 +2,28 @@ package stirling.software.common.util;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import lombok.extern.slf4j.Slf4j;
/**
* Factory for creating executors backed by virtual threads (Java 21+). Virtual threads are
* lightweight, managed by the JVM, and ideal for I/O-bound tasks. They eliminate the need for
* thread pool sizing since thousands can run concurrently with minimal overhead.
*/
public final class ExecutorFactory {
@Slf4j
public class ExecutorFactory {
private ExecutorFactory() {}
/** Creates an {@link ExecutorService} that starts a new virtual thread for each task. */
public static ExecutorService newVirtualThreadExecutor() {
return Executors.newVirtualThreadPerTaskExecutor();
}
/**
* Creates an ExecutorService using virtual threads if available (Java 21+), or falls back to a
* cached thread pool on older Java versions.
* Creates a {@link ScheduledExecutorService} backed by a single virtual thread. Useful for
* periodic/delayed tasks that should not pin a platform thread.
*/
public static ExecutorService newVirtualOrCachedThreadExecutor() {
try {
ExecutorService executor =
(ExecutorService)
Executors.class
.getMethod("newVirtualThreadPerTaskExecutor")
.invoke(null);
return executor;
} catch (NoSuchMethodException e) {
log.debug("Virtual threads not available; falling back to cached thread pool.");
} catch (Exception e) {
log.debug("Error initializing virtual thread executor: {}", e.getMessage(), e);
}
return Executors.newCachedThreadPool();
public static ScheduledExecutorService newSingleVirtualThreadScheduledExecutor() {
return Executors.newSingleThreadScheduledExecutor(
Thread.ofVirtual().name("scheduled-vt-", 0).factory());
}
}
@@ -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,39 @@ 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");
} else {
// Register directories eagerly so the first @Scheduled tick does not warn.
for (Path rootDir : this.rootDirs) {
if (Files.exists(rootDir)) {
try {
recursivelyRegisterEntry(rootDir);
} catch (IOException e) {
log.error("Failed to register monitoring for {}", rootDir, e);
}
}
}
}
}
private boolean shouldNotProcess(Path path) {
@@ -85,13 +116,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);
}
}
}
}
@@ -0,0 +1,217 @@
package stirling.software.common.util;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.channels.OverlappingFileLockException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Locale;
import org.springframework.stereotype.Component;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import stirling.software.common.model.ApplicationProperties;
import stirling.software.common.model.ApplicationProperties.AutoPipeline.FileReadiness;
/**
* Stateless safety checker that decides whether a file is stable and ready for pipeline processing.
* Call {@link #isReady(Path)} before moving or processing any file picked up from a watched folder.
*
* <p>A file is considered ready when ALL of the following hold:
*
* <ol>
* <li>The file exists on disk.
* <li>The path refers to a regular file, not a directory.
* <li>The file's extension matches the configured allow-list (if one is set).
* <li>The file has not been modified within the configured settle window ({@code
* settleTimeMillis}), meaning it is no longer being written.
* <li>The file size is stable: two reads separated by {@code sizeCheckDelayMillis} return the
* same value. This catches active copies on Linux/macOS where advisory file locking alone
* cannot detect a mid-copy file.
* <li>An exclusive file-system lock can be acquired, confirming no other process holds it.
* </ol>
*
* <p>All behaviour is controlled through {@link FileReadiness} inside {@link
* ApplicationProperties.AutoPipeline}. Setting {@code enabled: false} makes every call return
* {@code true} so the checker is a no-op drop-in.
*/
@Component
@RequiredArgsConstructor
@Slf4j
public class FileReadinessChecker {
private final ApplicationProperties applicationProperties;
/**
* Returns {@code true} when the file at {@code path} passes every readiness check and is safe
* to hand off to the pipeline for processing. Returns {@code false} when any check fails; the
* caller should skip the file and retry on the next scan cycle.
*/
public boolean isReady(Path path) {
FileReadiness config = applicationProperties.getAutoPipeline().getFileReadiness();
if (!config.isEnabled()) {
return true;
}
if (!existsAsRegularFile(path)) {
return false;
}
if (!isExtensionAllowed(path, config.getAllowedExtensions())) {
return false;
}
if (!hasSettled(path, config.getSettleTimeMillis())) {
return false;
}
if (!hasSizeStabilized(path, config.getSizeCheckDelayMillis())) {
return false;
}
if (isLocked(path)) {
return false;
}
return true;
}
// -------------------------------------------------------------------------
// Individual checks
// -------------------------------------------------------------------------
private boolean existsAsRegularFile(Path path) {
if (!Files.exists(path)) {
log.debug("File does not exist, skipping: {}", path);
return false;
}
if (!Files.isRegularFile(path)) {
log.debug("Path is not a regular file (directory or symlink?), skipping: {}", path);
return false;
}
return true;
}
/**
* Returns {@code true} when {@code allowedExtensions} is empty (no filter) or when the file's
* extension (case-insensitive) appears in the list.
*/
private boolean isExtensionAllowed(Path path, List<String> allowedExtensions) {
if (allowedExtensions == null || allowedExtensions.isEmpty()) {
return true;
}
String filename = path.getFileName().toString();
String extension =
filename.contains(".")
? filename.substring(filename.lastIndexOf('.') + 1).toLowerCase(Locale.ROOT)
: "";
boolean allowed =
allowedExtensions.stream().anyMatch(ext -> ext.equalsIgnoreCase(extension));
if (!allowed) {
log.debug(
"File '{}' has extension '{}' which is not in the allowed list {}, skipping",
filename,
extension,
allowedExtensions);
}
return allowed;
}
/**
* Returns {@code true} when the file's last-modified timestamp is at least {@code
* settleTimeMillis} milliseconds in the past, indicating the write has completed and the file
* has "settled".
*/
private boolean hasSettled(Path path, long settleTimeMillis) {
try {
long lastModified = Files.getLastModifiedTime(path).toMillis();
long ageMillis = System.currentTimeMillis() - lastModified;
boolean settled = ageMillis >= settleTimeMillis;
if (!settled) {
log.debug(
"File '{}' was modified {}ms ago (settle threshold: {}ms), not yet ready",
path.getFileName(),
ageMillis,
settleTimeMillis);
}
return settled;
} catch (IOException e) {
log.warn(
"Could not read last-modified time for '{}', treating as not settled: {}",
path,
e.getMessage());
return false;
}
}
/**
* Returns {@code true} when the file size is the same before and after a short pause of {@code
* sizeCheckDelayMillis} milliseconds. A size change indicates another process is still
* appending to the file. This is the primary write-detection mechanism on Linux/macOS, where
* mandatory file locking is not enforced by the OS.
*/
private boolean hasSizeStabilized(Path path, long sizeCheckDelayMillis) {
try {
long sizeBefore = Files.size(path);
Thread.sleep(sizeCheckDelayMillis);
long sizeAfter = Files.size(path);
boolean stable = sizeBefore == sizeAfter;
if (!stable) {
log.debug(
"File '{}' size changed from {} to {} bytes during stability check,"
+ " not yet ready",
path.getFileName(),
sizeBefore,
sizeAfter);
}
return stable;
} catch (IOException e) {
log.warn(
"Could not read file size for '{}', treating as unstable: {}",
path,
e.getMessage());
return false;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.warn(
"Size stability check interrupted for '{}', treating as unstable",
path.getFileName());
return false;
}
}
/**
* Returns {@code true} when an exclusive file-system lock cannot be acquired, which indicates
* another process still holds the file open for writing.
*
* <p>{@link OverlappingFileLockException} is also treated as locked: the JVM already holds a
* lock on this file (e.g. from another thread), so it is unsafe to process.
*/
private boolean isLocked(Path path) {
try (RandomAccessFile raf = new RandomAccessFile(path.toFile(), "rw");
FileChannel channel = raf.getChannel()) {
FileLock lock = channel.tryLock();
if (lock == null) {
log.debug("File '{}' is locked by another process", path.getFileName());
return true;
}
lock.release();
return false;
} catch (OverlappingFileLockException e) {
log.debug("File '{}' is already locked by this JVM", path.getFileName());
return true;
} catch (IOException e) {
log.debug(
"Could not acquire lock on '{}', treating as locked: {}",
path.getFileName(),
e.getMessage());
return true;
}
}
}
@@ -44,9 +44,7 @@ public class FileToPdf {
sanitizeHtmlContent(
new String(fileBytes, StandardCharsets.UTF_8),
customHtmlSanitizer);
Files.write(
tempInputFile.getPath(),
sanitizedHtml.getBytes(StandardCharsets.UTF_8));
Files.writeString(tempInputFile.getPath(), sanitizedHtml);
} else if (fileName.toLowerCase(Locale.ROOT).endsWith(".zip")) {
Files.write(tempInputFile.getPath(), fileBytes);
sanitizeHtmlFilesInZip(
@@ -100,6 +98,13 @@ public class FileToPdf {
while (entry != null) {
Path filePath =
tempUnzippedDir.getPath().resolve(sanitizeZipFilename(entry.getName()));
Path normalizedTargetDir =
tempUnzippedDir.getPath().toAbsolutePath().normalize();
Path normalizedFilePath = filePath.toAbsolutePath().normalize();
if (!normalizedFilePath.startsWith(normalizedTargetDir)) {
throw new IOException(
"Zip entry path escapes target directory: " + entry.getName());
}
if (!entry.isDirectory()) {
Files.createDirectories(filePath.getParent());
if (entry.getName().toLowerCase(Locale.ROOT).endsWith(".html")
@@ -108,8 +113,7 @@ public class FileToPdf {
new String(zipIn.readAllBytes(), StandardCharsets.UTF_8);
String sanitizedContent =
sanitizeHtmlContent(content, customHtmlSanitizer);
Files.write(
filePath, sanitizedContent.getBytes(StandardCharsets.UTF_8));
Files.writeString(filePath, sanitizedContent);
} else {
Files.copy(zipIn, filePath);
}
@@ -1,4 +1,4 @@
package stirling.software.proprietary.util;
package stirling.software.common.util;
import java.io.IOException;
import java.util.Arrays;
@@ -1,10 +1,11 @@
package stirling.software.proprietary.util;
package stirling.software.common.util;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
@@ -16,8 +17,10 @@ import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Pattern;
import org.apache.pdfbox.cos.COSArray;
import org.apache.pdfbox.cos.COSBase;
import org.apache.pdfbox.cos.COSDictionary;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.pdmodel.PDDocument;
@@ -46,9 +49,7 @@ import lombok.experimental.UtilityClass;
import lombok.extern.slf4j.Slf4j;
import stirling.software.common.model.ApplicationProperties;
import stirling.software.common.util.ApplicationContextProvider;
import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.RegexPatternUtils;
import stirling.software.common.model.FormFieldWithCoordinates;
@Slf4j
@UtilityClass
@@ -67,6 +68,17 @@ public class FormUtils {
public final Set<String> CHOICE_FIELD_TYPES =
Set.of(FIELD_TYPE_COMBOBOX, FIELD_TYPE_LISTBOX, FIELD_TYPE_RADIO);
/**
* Threshold in PDF points for considering two widgets to be on the same line. Fields whose
* y-coordinates differ by less than this value are sorted left-to-right by x-coordinate instead
* of top-to-bottom.
*/
private static final float SAME_LINE_THRESHOLD_PT = 10.0f;
private static final Pattern HEX_UUID_PATTERN =
Pattern.compile("^[0-9a-fA-F]{8}[0-9a-fA-F]{24,}$");
private static final Pattern WHITESPACE_PATTERN = Pattern.compile("\\s+");
/**
* Returns a normalized logical type string for the supplied PDFBox field instance. Centralized
* so all callers share identical mapping logic.
@@ -109,6 +121,8 @@ public class FormUtils {
List<FormFieldInfo> fields = new ArrayList<>();
Map<String, Integer> typeCounters = new HashMap<>();
Map<Integer, Integer> pageOrderCounters = new HashMap<>();
Map<COSDictionary, Integer> annotationPageMap = buildAnnotationPageMap(document);
for (PDField field : acroForm.getFieldTree()) {
if (!(field instanceof PDTerminalField terminalField)) {
continue;
@@ -123,9 +137,9 @@ public class FormUtils {
continue;
}
String currentValue = safeValue(terminalField);
String currentValue = safeFieldValue(terminalField);
boolean required = field.isRequired();
int pageIndex = resolveFirstWidgetPageIndex(document, terminalField);
int pageIndex = resolveFirstWidgetPageIndex(document, terminalField, annotationPageMap);
List<String> options = resolveOptions(terminalField);
String tooltip = resolveTooltip(terminalField);
int typeIndex = typeCounters.merge(type, 1, Integer::sum);
@@ -164,6 +178,396 @@ public class FormUtils {
return Collections.unmodifiableList(fields);
}
/**
* Extract form fields with widget coordinates for the interactive form viewer.
*
* @param document PDF document
* @return List of form fields with coordinates and metadata
*/
public List<FormFieldWithCoordinates> extractFormFieldsWithCoordinates(PDDocument document) {
if (document == null) return List.of();
PDAcroForm acroForm = getAcroFormSafely(document);
if (acroForm == null) return List.of();
List<FormFieldWithCoordinates> fields = new ArrayList<>();
Map<String, Integer> typeCounters = new HashMap<>();
Map<COSDictionary, Integer> annotationPageMap = buildAnnotationPageMap(document);
for (PDField field : acroForm.getFieldTree()) {
if (!(field instanceof PDTerminalField terminalField)) {
continue;
}
String type = detectFieldType(terminalField);
String name =
Optional.ofNullable(field.getFullyQualifiedName())
.orElseGet(field::getPartialName);
if (name == null || name.isBlank()) {
continue;
}
String currentValue = safeFieldValue(terminalField);
boolean required = field.isRequired();
boolean readOnly = field.isReadOnly();
List<String> options = resolveOptions(terminalField);
List<String> displayOptions = resolveDisplayOptions(terminalField);
String tooltip = resolveTooltip(terminalField);
int typeIndex = typeCounters.merge(type, 1, Integer::sum);
String displayLabel =
deriveDisplayLabel(field, name, tooltip, type, typeIndex, options);
boolean multiSelect = resolveMultiSelect(terminalField);
boolean multiline =
terminalField instanceof PDTextField
&& ((PDTextField) terminalField).isMultiline();
// Extract widget coordinates
List<FormFieldWithCoordinates.WidgetCoordinates> widgets =
extractWidgetCoordinates(document, terminalField, annotationPageMap);
// Only include displayOptions when they differ from export options
List<String> displayOptsToSend = null;
if (displayOptions != null
&& !displayOptions.isEmpty()
&& !displayOptions.equals(options)) {
displayOptsToSend = displayOptions;
}
fields.add(
FormFieldWithCoordinates.builder()
.name(name)
.label(displayLabel)
.type(type)
.value(currentValue)
.options(options.isEmpty() ? null : options)
.displayOptions(displayOptsToSend)
.required(required)
.readOnly(readOnly)
.multiSelect(multiSelect)
.multiline(multiline)
.tooltip(tooltip)
.widgets(widgets.isEmpty() ? null : widgets)
.build());
}
// Sort by page and position
fields.sort(new FieldCoordinateComparator());
log.debug("Total fields processed: {}", fields.size());
log.debug(
"Fields WITH widgets: {}",
fields.stream()
.filter(f -> f.getWidgets() != null && !f.getWidgets().isEmpty())
.count());
log.debug(
"Fields WITHOUT widgets: {}",
fields.stream()
.filter(f -> f.getWidgets() == null || f.getWidgets().isEmpty())
.count());
fields.stream()
.filter(f -> f.getWidgets() == null || f.getWidgets().isEmpty())
.forEach(
f ->
log.debug(
"Field '{}' type={} has NO widget coordinates",
f.getName(),
f.getType()));
return Collections.unmodifiableList(fields);
}
/**
* Extract widget coordinates for a form field.
*
* @param document PDF document
* @param field Terminal field
* @return List of widget coordinates
*/
private List<FormFieldWithCoordinates.WidgetCoordinates> extractWidgetCoordinates(
PDDocument document,
PDTerminalField field,
Map<COSDictionary, Integer> annotationPageMap) {
List<FormFieldWithCoordinates.WidgetCoordinates> result = new ArrayList<>();
List<PDAnnotationWidget> widgets = field.getWidgets();
log.debug(
"Field '{}' type={} has {} widgets",
field.getFullyQualifiedName(),
field.getClass().getSimpleName(),
widgets != null ? widgets.size() : 0);
if (widgets == null || widgets.isEmpty()) {
// Some fields (especially text fields) might be their own widget annotation
log.trace(
"Field '{}' has no widgets, checking if field acts as its own annotation",
field.getFullyQualifiedName());
try {
COSDictionary fieldDict = field.getCOSObject();
COSBase rectBase = fieldDict.getDictionaryObject(COSName.RECT);
if (rectBase instanceof COSArray rectArray) {
int pageIndex =
findPageIndexForAnnotation(document, fieldDict, annotationPageMap);
if (pageIndex >= 0) {
PDRectangle rectangle = new PDRectangle(rectArray);
result.add(
createWidgetCoordinates(
document, rectangle, pageIndex, null, field));
} else {
log.warn(
"Found rectangle for field '{}' but could not resolve page index",
field.getFullyQualifiedName());
}
}
} catch (Exception e) {
log.debug(
"Could not extract direct rectangle for field '{}': {}",
field.getFullyQualifiedName(),
e.getMessage());
}
return result;
}
// For radio buttons, pre-resolve export values per widget
List<String> exportValues = null;
if (field instanceof PDRadioButton radio) {
exportValues = radio.getExportValues();
}
for (int i = 0; i < widgets.size(); i++) {
PDAnnotationWidget widget = widgets.get(i);
try {
PDRectangle rectangle = widget.getRectangle();
if (rectangle == null) {
log.warn(
"Field '{}' widget {} has NULL rectangle",
field.getFullyQualifiedName(),
i);
continue;
}
int pageIndex = resolveWidgetPageIndex(document, widget, annotationPageMap);
if (pageIndex < 0) {
log.warn(
"Field '{}' widget {} could not resolve page index",
field.getFullyQualifiedName(),
i);
continue;
}
// Resolve export value for radio/checkbox widgets
String exportValue = null;
if (exportValues != null && i < exportValues.size()) {
exportValue = exportValues.get(i);
} else if (field instanceof PDButton) {
// Fall back to appearance state name from the widget's normal appearance
try {
var ap = widget.getAppearance();
if (ap != null && ap.getNormalAppearance() != null) {
var normalAp = ap.getNormalAppearance();
if (normalAp.isSubDictionary()) {
for (var cosName : normalAp.getSubDictionary().keySet()) {
String key = cosName.getName();
if (!"Off".equals(key)) {
exportValue = key;
break;
}
}
}
}
} catch (Exception e) {
log.trace(
"Could not extract export value for widget in '{}': {}",
field.getFullyQualifiedName(),
e.getMessage());
}
}
result.add(
createWidgetCoordinates(
document, rectangle, pageIndex, exportValue, field));
} catch (Exception e) {
log.debug(
"Failed to extract coordinates for widget in field '{}': {}",
field.getFullyQualifiedName(),
e.getMessage());
}
}
return result;
}
private FormFieldWithCoordinates.WidgetCoordinates createWidgetCoordinates(
PDDocument document,
PDRectangle rectangle,
int pageIndex,
String exportValue,
PDTerminalField field) {
if (pageIndex < 0 || pageIndex >= document.getNumberOfPages()) {
return null;
}
PDPage page = document.getPage(pageIndex);
PDRectangle cropBox = page.getCropBox();
// Use CropBox dimensions for the y-flip.
// Note: getWidth() and getHeight() return dimensions BEFORE rotation.
float cropHeight = cropBox.getHeight();
// Get absolute widget coordinates (in MediaBox space, un-rotated)
float pdfX = rectangle.getLowerLeftX();
float pdfY = rectangle.getLowerLeftY();
float width = rectangle.getWidth();
float height = rectangle.getHeight();
// Adjust relative to CropBox origin
float relativeX = pdfX - cropBox.getLowerLeftX();
float relativeY = pdfY - cropBox.getLowerLeftY();
// Convert from PDF lower-left origin to CSS upper-left origin (y-flip).
// Widget /Rect coordinates are always in un-rotated PDF user space.
// The embedpdf viewer wraps all page content inside a <Rotate> CSS
// component that handles visual rotation we must NOT apply any
// rotation transform here, or widgets would be double-rotated.
float finalX = relativeX;
float finalY = cropHeight - relativeY - height;
float finalW = width;
float finalH = height;
// Validate coordinates are within reasonable bounds
if (finalX < -1.0f
|| finalY < -1.0f
|| finalX > cropBox.getWidth() * 2 // Allow some horizontal overflow
|| finalY > cropHeight + 1.0f) {
log.warn(
"Widget coordinates out of bounds for field '{}': page={}, x={}, y={}, w={}, h={}",
field.getFullyQualifiedName(),
pageIndex,
finalX,
finalY,
finalW,
finalH);
return null;
}
return FormFieldWithCoordinates.WidgetCoordinates.builder()
.pageIndex(pageIndex)
.x(finalX)
.y(finalY)
.width(finalW)
.height(finalH)
.exportValue(exportValue)
.fontSize(extractFontSize(field))
.build();
}
/**
* Repairs widgets with missing page references by scanning all pages and setting the /P entry
* for orphan widgets.
*
* <p>This should be called BEFORE extracting form field coordinates.
*
* @param document PDF document to repair
*/
public void repairMissingWidgetPageReferences(PDDocument document) {
try {
PDAcroForm acroForm = getAcroFormSafely(document);
if (acroForm == null) {
return;
}
log.debug("Checking for widgets with missing page references...");
int repairedCount = 0;
Map<COSDictionary, Integer> annotationPageMap = buildAnnotationPageMap(document);
for (PDField field : acroForm.getFieldTree()) {
if (!(field instanceof PDTerminalField terminalField)) {
continue;
}
List<PDAnnotationWidget> widgets = terminalField.getWidgets();
if (widgets == null || widgets.isEmpty()) {
continue;
}
for (PDAnnotationWidget widget : widgets) {
if (widget.getPage() == null) {
Integer pageIndex = annotationPageMap.get(widget.getCOSObject());
if (pageIndex != null && pageIndex >= 0) {
PDPage foundPage = document.getPage(pageIndex);
widget.setPage(foundPage);
repairedCount++;
log.debug(
"Repaired widget for field '{}' - set page reference via map",
field.getFullyQualifiedName());
} else {
log.warn(
"Could not find page for widget in field '{}'",
field.getFullyQualifiedName());
}
}
}
}
if (repairedCount > 0) {
log.debug(
"Successfully repaired {} widgets with missing page references",
repairedCount);
} else {
log.debug("No widgets needed repair");
}
} catch (Exception e) {
log.error("Error repairing widget page references: {}", e.getMessage(), e);
}
}
private int findPageIndexForAnnotation(
PDDocument document,
COSDictionary annotDict,
Map<COSDictionary, Integer> annotationPageMap) {
try {
// Method 0: Check the pre-built lookup map (fastest)
if (annotationPageMap != null) {
Integer idx = annotationPageMap.get(annotDict);
if (idx != null) {
return idx;
}
}
// Method 1: Check the /P entry if it points to a page
COSBase base = annotDict.getDictionaryObject(COSName.P);
COSDictionary pageDict = (base instanceof COSDictionary c) ? c : null;
if (pageDict != null) {
for (int i = 0; i < document.getNumberOfPages(); i++) {
if (document.getPage(i).getCOSObject() == pageDict) {
return i;
}
}
}
// Method 2: Fallback search through all pages' annotations
for (int i = 0; i < document.getNumberOfPages(); i++) {
PDPage page = document.getPage(i);
List<PDAnnotation> annotations = page.getAnnotations();
if (annotations != null) {
for (PDAnnotation annot : annotations) {
if (annot != null && annot.getCOSObject() == annotDict) {
return i;
}
}
}
}
} catch (Exception e) {
log.trace("Error finding page for annotation: {}", e.getMessage());
}
return -1;
}
/**
* Build a single record object (field-name -> value placeholder) that can be directly submitted
* to /api/v1/form/fill as the 'data' JSON. For checkboxes a boolean false is supplied unless
@@ -312,7 +716,24 @@ public class FormUtils {
return;
}
flattenViaRendering(document, acroForm);
if (acroForm == null) {
return;
}
// Use PDFBox's built-in field flattening which bakes form field values
// into the page content stream as static text/graphics, removing the
// interactive form structure but preserving all other document content
// (images, text, annotations, etc.) at full quality.
try {
ensureAppearances(acroForm);
acroForm.flatten();
} catch (Exception e) {
log.warn(
"PDFBox acroForm.flatten() failed, falling back to rendering: {}",
e.getMessage(),
e);
flattenViaRendering(document, acroForm);
}
}
private void rebuildDocumentFromImages(PDDocument document, PDFRenderer renderer, int dpi)
@@ -385,7 +806,7 @@ public class FormUtils {
PDPage page = widget.getPage();
if (page == null) {
page = resolveWidgetPage(document, widget);
page = resolveWidgetPage(document, widget, null);
if (page != null) {
widget.setPage(page);
}
@@ -818,8 +1239,22 @@ public class FormUtils {
return states;
}
private String safeValue(PDTerminalField field) {
public String safeValue(String value) {
return value != null ? value : "";
}
private String safeFieldValue(PDTerminalField field) {
try {
// PDChoice.getValueAsString() returns a raw COS string representation
// that doesn't reliably reflect the selected value. Use getValue()
// which returns the proper List<String> of selected options.
if (field instanceof PDChoice choiceField) {
List<String> selected = choiceField.getValue();
if (selected == null || selected.isEmpty()) {
return null;
}
return String.join(",", selected);
}
return field.getValueAsString();
} catch (Exception e) {
log.debug(
@@ -833,14 +1268,25 @@ public class FormUtils {
List<String> resolveOptions(PDTerminalField field) {
try {
if (field instanceof PDChoice choice) {
List<String> display = choice.getOptionsDisplayValues();
if (display != null && !display.isEmpty()) {
return new ArrayList<>(display);
}
LinkedHashSet<String> allowed = new LinkedHashSet<>();
List<String> exportValues = choice.getOptionsExportValues();
if (exportValues != null && !exportValues.isEmpty()) {
return new ArrayList<>(exportValues);
List<String> displayValues = choice.getOptionsDisplayValues();
if (exportValues != null) {
exportValues.stream()
.filter(Objects::nonNull)
.map(String::trim)
.filter(s -> !s.isEmpty())
.forEach(allowed::add);
}
if (displayValues != null) {
displayValues.stream()
.filter(Objects::nonNull)
.map(String::trim)
.filter(s -> !s.isEmpty())
.forEach(allowed::add);
}
return new ArrayList<>(allowed);
} else if (field instanceof PDRadioButton radio) {
List<String> exports = radio.getExportValues();
if (exports != null && !exports.isEmpty()) {
@@ -861,6 +1307,29 @@ public class FormUtils {
return Collections.emptyList();
}
/**
* Returns the display-value labels for a choice field's options. For radio / checkbox this
* returns an empty list (no separate display values). For PDChoice fields, if the PDF provides
* distinct display values, those are returned; otherwise an empty list (indicating that the
* export values from {@link #resolveOptions} should be shown directly).
*/
List<String> resolveDisplayOptions(PDTerminalField field) {
try {
if (field instanceof PDChoice choice) {
List<String> display = choice.getOptionsDisplayValues();
if (display != null && !display.isEmpty()) {
return new ArrayList<>(display);
}
}
} catch (Exception e) {
log.debug(
"Failed to resolve display options for field '{}': {}",
field.getFullyQualifiedName(),
e.getMessage());
}
return Collections.emptyList();
}
private boolean resolveMultiSelect(PDTerminalField field) {
if (field instanceof PDListBox listBox) {
try {
@@ -875,6 +1344,44 @@ public class FormUtils {
return false;
}
private Float extractFontSize(PDTerminalField field) {
try {
String da = null;
if (field instanceof PDVariableText vt) {
da = vt.getDefaultAppearance();
}
if (da == null || da.isBlank()) {
// Check parent/acroform default appearance if field's is missing
PDAcroForm form = field.getAcroForm();
if (form != null) {
da = form.getDefaultAppearance();
}
}
if (da != null && !da.isBlank()) {
// Standard DA looks like: /Helv 12 Tf 0 g
// We want the number before 'Tf'
String[] tokens = WHITESPACE_PATTERN.split(da);
for (int i = 0; i < tokens.length; i++) {
if ("Tf".equals(tokens[i]) && i > 0) {
try {
float size = Float.parseFloat(tokens[i - 1]);
return size > 0 ? size : null;
} catch (NumberFormatException ignored) {
}
}
}
}
} catch (Exception e) {
log.trace(
"Could not extract font size for field '{}': {}",
field.getFullyQualifiedName(),
e.getMessage());
}
return null;
}
private boolean isSettableCheckBoxState(String state) {
if (state == null) return false;
String trimmed = state.trim();
@@ -952,6 +1459,12 @@ public class FormUtils {
if (simplified.isEmpty()) return true;
// Detect UUID-like hex strings (e.g. "cdc47b7041524571 7b2d93017fe77bf7")
// Standard UUIDs are 32 hex characters; require at least that to avoid
// false positives on short hex-like field names.
String nospaces = WHITESPACE_PATTERN.matcher(simplified).replaceAll("");
if (nospaces.length() >= 32 && HEX_UUID_PATTERN.matcher(nospaces).matches()) return true;
return patterns.getGenericFieldNamePattern().matcher(simplified).matches()
|| patterns.getSimpleFormFieldPattern().matcher(simplified).matches()
|| patterns.getOptionalTNumericPattern().matcher(simplified).matches();
@@ -1007,7 +1520,7 @@ public class FormUtils {
PDAnnotationWidget widget = widgets.get(0);
PDRectangle originalRectangle = cloneRectangle(widget.getRectangle());
PDPage page = resolveWidgetPage(document, widget);
PDPage page = resolveWidgetPage(document, widget, null);
if (page == null || originalRectangle == null) {
log.warn(
"Unable to resolve widget page or rectangle for '{}'; skipping",
@@ -1064,7 +1577,7 @@ public class FormUtils {
desiredName,
modification.label(),
resolvedType,
determineWidgetPageIndex(document, widget),
determineWidgetPageIndex(document, widget, null),
originalRectangle.getLowerLeftX(),
originalRectangle.getLowerLeftY(),
originalRectangle.getWidth(),
@@ -1205,59 +1718,43 @@ public class FormUtils {
return null;
}
private int resolveFirstWidgetPageIndex(PDDocument document, PDTerminalField field) {
private int resolveFirstWidgetPageIndex(
PDDocument document,
PDTerminalField field,
Map<COSDictionary, Integer> annotationPageMap) {
List<PDAnnotationWidget> widgets = field.getWidgets();
if (widgets == null || widgets.isEmpty()) {
return -1;
}
Map<PDAnnotationWidget, Integer> widgetPageFallbacks = null;
for (PDAnnotationWidget widget : widgets) {
int idx = resolveWidgetPageIndex(document, widget);
int idx = resolveWidgetPageIndex(document, widget, annotationPageMap);
if (idx >= 0) {
return idx;
}
try {
COSDictionary widgetDictionary = widget.getCOSObject();
if (widgetDictionary != null
&& widgetDictionary.getDictionaryObject(COSName.P) == null) {
if (widgetPageFallbacks == null) {
widgetPageFallbacks = buildWidgetPageFallbackMap(document);
}
Integer fallbackIndex = widgetPageFallbacks.get(widget);
if (fallbackIndex != null && fallbackIndex >= 0) {
return fallbackIndex;
}
}
} catch (Exception e) {
log.debug(
"Failed to inspect widget page reference for field '{}': {}",
field.getFullyQualifiedName(),
e.getMessage());
}
}
return -1;
}
private int resolveWidgetPageIndex(PDDocument document, PDAnnotationWidget widget) {
private int resolveWidgetPageIndex(
PDDocument document,
PDAnnotationWidget widget,
Map<COSDictionary, Integer> annotationPageMap) {
if (document == null || widget == null) {
return -1;
}
try {
COSDictionary widgetDictionary = widget.getCOSObject();
if (widgetDictionary != null
&& widgetDictionary.getDictionaryObject(COSName.P) == null) {
Map<PDAnnotationWidget, Integer> fallback = buildWidgetPageFallbackMap(document);
Integer index = fallback.get(widget);
if (index != null) {
return index;
}
// Method 0: Check the pre-built lookup map (fastest)
if (annotationPageMap != null) {
Integer idx = annotationPageMap.get(widget.getCOSObject());
if (idx != null) {
return idx;
}
} catch (Exception e) {
log.debug("Widget page lookup via fallback map failed: {}", e.getMessage());
}
try {
PDPage page = widget.getPage();
if (page != null) {
// indexOf is O(N), still slower than map but better than scanning annotations
int idx = document.getPages().indexOf(page);
if (idx >= 0) {
return idx;
@@ -1267,14 +1764,36 @@ public class FormUtils {
log.debug("Widget page lookup failed: {}", e.getMessage());
}
// Method 1: Check the /P entry if it points to a page
try {
COSDictionary widgetDictionary = widget.getCOSObject();
if (widgetDictionary != null) {
COSBase base = widgetDictionary.getDictionaryObject(COSName.P);
COSDictionary pageDict = (base instanceof COSDictionary c) ? c : null;
if (pageDict != null) {
for (int i = 0; i < document.getNumberOfPages(); i++) {
if (document.getPage(i).getCOSObject() == pageDict) {
return i;
}
}
}
}
} catch (Exception e) {
log.debug("Widget page lookup via /P entry failed: {}", e.getMessage());
}
// Method 2: Fallback search through all pages' annotations
int pageCount = document.getNumberOfPages();
COSDictionary widgetDict = widget.getCOSObject();
for (int i = 0; i < pageCount; i++) {
try {
PDPage candidate = document.getPage(i);
List<PDAnnotation> annotations = candidate.getAnnotations();
for (PDAnnotation annotation : annotations) {
if (annotation == widget) {
return i;
if (annotations != null) {
for (PDAnnotation annot : annotations) {
if (annot != null && annot.getCOSObject() == widgetDict) {
return i;
}
}
}
} catch (IOException e) {
@@ -1317,7 +1836,7 @@ public class FormUtils {
List<PDAnnotationWidget> widgets = field.getWidgets();
if (widgets != null) {
for (PDAnnotationWidget widget : widgets) {
PDPage page = resolveWidgetPage(document, widget);
PDPage page = resolveWidgetPage(document, widget, null);
if (page != null) {
page.getAnnotations().remove(widget);
}
@@ -1437,7 +1956,10 @@ public class FormUtils {
rectangle.getHeight());
}
private PDPage resolveWidgetPage(PDDocument document, PDAnnotationWidget widget) {
private PDPage resolveWidgetPage(
PDDocument document,
PDAnnotationWidget widget,
Map<COSDictionary, Integer> annotationPageMap) {
if (widget == null) {
return null;
}
@@ -1445,7 +1967,7 @@ public class FormUtils {
if (page != null) {
return page;
}
int pageIndex = determineWidgetPageIndex(document, widget);
int pageIndex = determineWidgetPageIndex(document, widget, annotationPageMap);
if (pageIndex >= 0) {
try {
return document.getPage(pageIndex);
@@ -1456,11 +1978,21 @@ public class FormUtils {
return null;
}
private int determineWidgetPageIndex(PDDocument document, PDAnnotationWidget widget) {
private int determineWidgetPageIndex(
PDDocument document,
PDAnnotationWidget widget,
Map<COSDictionary, Integer> annotationPageMap) {
if (document == null || widget == null) {
return -1;
}
if (annotationPageMap != null) {
Integer idx = annotationPageMap.get(widget.getCOSObject());
if (idx != null) {
return idx;
}
}
PDPage directPage = widget.getPage();
if (directPage != null) {
int index = 0;
@@ -1488,6 +2020,33 @@ public class FormUtils {
return -1;
}
/**
* Build a map of annotation COS dictionaries to their respective page index. Scan once
* per-document to avoid O(N^2) lookups during field extraction.
*/
public Map<COSDictionary, Integer> buildAnnotationPageMap(PDDocument document) {
if (document == null) {
return Collections.emptyMap();
}
Map<COSDictionary, Integer> map = new HashMap<>();
int pageCount = document.getNumberOfPages();
for (int i = 0; i < pageCount; i++) {
try {
PDPage page = document.getPage(i);
List<PDAnnotation> annotations = page.getAnnotations();
for (PDAnnotation annot : annotations) {
if (annot != null) {
map.putIfAbsent(annot.getCOSObject(), i);
}
}
} catch (Exception e) {
log.debug("Failed to index annotations for page {}: {}", i, e.getMessage());
}
}
return map;
}
private Map<PDAnnotationWidget, Integer> buildWidgetPageFallbackMap(PDDocument document) {
if (document == null) {
return Collections.emptyMap();
@@ -1760,4 +2319,46 @@ public class FormUtils {
boolean multiSelect,
String tooltip,
int pageOrder) {}
/**
* Comparator for sorting form fields by page, then vertically (top-to-bottom), then
* horizontally (left-to-right) for fields on approximately the same line.
*/
static final class FieldCoordinateComparator implements Comparator<FormFieldWithCoordinates> {
private static int firstWidgetPageIndex(FormFieldWithCoordinates f) {
return (f.getWidgets() != null && !f.getWidgets().isEmpty())
? f.getWidgets().get(0).getPageIndex()
: -1;
}
private static float firstWidgetY(FormFieldWithCoordinates f) {
return (f.getWidgets() != null && !f.getWidgets().isEmpty())
? f.getWidgets().get(0).getY()
: 0;
}
private static float firstWidgetX(FormFieldWithCoordinates f) {
return (f.getWidgets() != null && !f.getWidgets().isEmpty())
? f.getWidgets().get(0).getX()
: 0;
}
@Override
public int compare(FormFieldWithCoordinates a, FormFieldWithCoordinates b) {
int pageA = firstWidgetPageIndex(a);
int pageB = firstWidgetPageIndex(b);
int pageCompare = Integer.compare(pageA, pageB);
if (pageCompare != 0) return pageCompare;
float yA = firstWidgetY(a);
float yB = firstWidgetY(b);
// Fields on approximately the same line should be sorted left-to-right
if (Math.abs(yA - yB) < SAME_LINE_THRESHOLD_PT) {
return Float.compare(firstWidgetX(a), firstWidgetX(b));
}
return Float.compare(yA, yB);
}
}
}
@@ -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
@@ -632,8 +649,10 @@ public class GeneralUtils {
}
public List<Integer> parsePageList(String[] pages, int totalPages, boolean oneBased) {
List<Integer> result = new ArrayList<>();
// Use LinkedHashSet to prevent duplicates from inflating size and triggering maxSize guard
Set<Integer> result = new LinkedHashSet<>();
int offset = oneBased ? 1 : 0;
int maxSize = Math.max(1000, totalPages * 3);
for (String page : pages) {
if ("all".equalsIgnoreCase(page)) {
@@ -649,8 +668,12 @@ public class GeneralUtils {
} else {
result.addAll(handlePart(page, totalPages, offset));
}
if (result.size() > maxSize) {
throw new IllegalArgumentException(
"Page list exceeds maximum allowed size of " + maxSize);
}
}
return result;
return new ArrayList<>(result);
}
/*
@@ -850,6 +873,36 @@ public class GeneralUtils {
settingsYaml.saveOverride(settingsPath);
}
/**
* Updates multiple settings in a single transaction. This ensures that nested settings (e.g.,
* oauth2.client.google.*) don't lose sibling values when partial updates are made.
*
* <p>Instead of multiple read-update-write cycles (which could cause race conditions), this
* method loads the YAML once, applies all updates, and saves once.
*
* @param settingsMap Map of dotted-notation keys to values to update
* @throws IOException if file read/write fails
*/
public void updateSettingsTransactional(Map<String, Object> settingsMap) throws IOException {
if (settingsMap == null || settingsMap.isEmpty()) {
return;
}
Path settingsPath = Paths.get(InstallationPathConfig.getSettingsPath());
YamlHelper settingsYaml = new YamlHelper(settingsPath);
// Apply all updates to the same YamlHelper instance
for (Map.Entry<String, Object> entry : settingsMap.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
String[] keyArray = key.split("\\.");
settingsYaml.updateValue(Arrays.asList(keyArray), value);
}
// Save only once after all updates are applied
settingsYaml.saveOverride(settingsPath);
}
/*
* Machine fingerprint generation with better error logging and fallbacks.
*
@@ -10,6 +10,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
@@ -33,6 +34,8 @@ import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
@Slf4j
public class PDFToFile {
private static final Pattern PATTERN =
Pattern.compile("(!\\[.*?\\])\\((?!images/)([^/)][^)]*?)\\)");
private final TempFileManager tempFileManager;
private final RuntimePathConfig runtimePathConfig;
@@ -107,56 +110,65 @@ public class PDFToFile {
File[] outputFiles =
Objects.requireNonNull(tempOutputDir.getPath().toFile().listFiles());
List<File> markdownFiles = new ArrayList<>();
List<File> imageFiles = new ArrayList<>();
// Convert HTML files to Markdown
// Convert HTML files to Markdown and collect image files
for (File outputFile : outputFiles) {
if (outputFile.getName().endsWith(".html")) {
String html = Files.readString(outputFile.toPath());
String markdown = htmlToMarkdownConverter.convert(html);
// Update image references to point to images/ folder
markdown = updateImageReferences(markdown);
String mdFileName = outputFile.getName().replace(".html", ".md");
File mdFile = new File(tempOutputDir.getPath().toFile(), mdFileName);
Files.writeString(mdFile.toPath(), markdown);
markdownFiles.add(mdFile);
} else if (!outputFile.getName().endsWith(".md")) {
// Collect non-HTML, non-MD files as images/assets
imageFiles.add(outputFile);
}
}
// If there's only one markdown file, return it directly
if (markdownFiles.size() == 1) {
fileName = pdfBaseName + ".md";
fileBytes = Files.readAllBytes(markdownFiles.get(0).toPath());
} else {
// Multiple files - create a zip
fileName = pdfBaseName + "ToMarkdown.zip";
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
// Always create a ZIP file
fileName = pdfBaseName + "ToMarkdown.zip";
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
try (ZipOutputStream zipOutputStream = new ZipOutputStream(byteArrayOutputStream)) {
// Add markdown files
for (File mdFile : markdownFiles) {
ZipEntry mdEntry = new ZipEntry(mdFile.getName());
zipOutputStream.putNextEntry(mdEntry);
Files.copy(mdFile.toPath(), zipOutputStream);
zipOutputStream.closeEntry();
}
// Add images and other assets
for (File file : outputFiles) {
if (!file.getName().endsWith(".html") && !file.getName().endsWith(".md")) {
ZipEntry assetEntry = new ZipEntry(file.getName());
zipOutputStream.putNextEntry(assetEntry);
Files.copy(file.toPath(), zipOutputStream);
zipOutputStream.closeEntry();
}
}
try (ZipOutputStream zipOutputStream = new ZipOutputStream(byteArrayOutputStream)) {
// Add markdown files to root of ZIP
for (File mdFile : markdownFiles) {
ZipEntry mdEntry = new ZipEntry(mdFile.getName());
zipOutputStream.putNextEntry(mdEntry);
Files.copy(mdFile.toPath(), zipOutputStream);
zipOutputStream.closeEntry();
}
fileBytes = byteArrayOutputStream.toByteArray();
// Add images and other assets to images/ folder
for (File imageFile : imageFiles) {
ZipEntry assetEntry = new ZipEntry("images/" + imageFile.getName());
zipOutputStream.putNextEntry(assetEntry);
Files.copy(imageFile.toPath(), zipOutputStream);
zipOutputStream.closeEntry();
}
}
fileBytes = byteArrayOutputStream.toByteArray();
}
return WebResponseUtils.bytesToWebResponse(
fileBytes, fileName, MediaType.APPLICATION_OCTET_STREAM);
}
/**
* Updates image references in markdown to point to the images/ folder. Matches patterns like
* ![alt](filename.png) and converts to ![alt](images/filename.png)
*/
private String updateImageReferences(String markdown) {
// Match markdown image syntax: ![alt text](image.png)
// Only update if the path doesn't already start with images/
return PATTERN.matcher(markdown).replaceAll("$1(images/$2)");
}
public ResponseEntity<byte[]> processPdfToHtml(MultipartFile inputFile)
throws IOException, InterruptedException {
if (!MediaType.APPLICATION_PDF_VALUE.equals(inputFile.getContentType())) {
@@ -1,8 +1,8 @@
package stirling.software.common.util;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.util.Locale;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
@@ -22,8 +22,11 @@ import stirling.software.common.service.CustomPDFDocumentFactory;
@Slf4j
public class PdfToCbzUtils {
public static byte[] convertPdfToCbz(
MultipartFile pdfFile, int dpi, CustomPDFDocumentFactory pdfDocumentFactory)
public static TempFile convertPdfToCbz(
MultipartFile pdfFile,
int dpi,
CustomPDFDocumentFactory pdfDocumentFactory,
TempFileManager tempFileManager)
throws IOException {
validatePdfFile(pdfFile);
@@ -33,7 +36,7 @@ public class PdfToCbzUtils {
throw ExceptionUtils.createPdfNoPages();
}
return createCbzFromPdf(document, dpi);
return createCbzFromPdf(document, dpi, tempFileManager);
}
}
@@ -53,46 +56,49 @@ public class PdfToCbzUtils {
}
}
private static byte[] createCbzFromPdf(PDDocument document, int dpi) throws IOException {
private static TempFile createCbzFromPdf(
PDDocument document, int dpi, TempFileManager tempFileManager) throws IOException {
PDFRenderer pdfRenderer = new PDFRenderer(document);
pdfRenderer.setSubsamplingAllowed(true); // Enable subsampling to reduce memory usage
try (ByteArrayOutputStream cbzOutputStream = new ByteArrayOutputStream();
ZipOutputStream zipOut = new ZipOutputStream(cbzOutputStream)) {
TempFile cbzTempFile = new TempFile(tempFileManager, ".cbz");
try {
try (ZipOutputStream zipOut =
new ZipOutputStream(Files.newOutputStream(cbzTempFile.getPath()))) {
int totalPages = document.getNumberOfPages();
int totalPages = document.getNumberOfPages();
for (int pageIndex = 0; pageIndex < totalPages; pageIndex++) {
final int currentPage = pageIndex;
try {
BufferedImage image =
ExceptionUtils.handleOomRendering(
currentPage + 1,
dpi,
() ->
pdfRenderer.renderImageWithDPI(
currentPage, dpi, ImageType.RGB));
for (int pageIndex = 0; pageIndex < totalPages; pageIndex++) {
final int currentPage = pageIndex;
try {
BufferedImage image =
ExceptionUtils.handleOomRendering(
currentPage + 1,
dpi,
() ->
pdfRenderer.renderImageWithDPI(
currentPage, dpi, ImageType.RGB));
String imageFilename =
String.format(Locale.ROOT, "page_%03d.png", currentPage + 1);
ZipEntry zipEntry = new ZipEntry(imageFilename);
zipOut.putNextEntry(zipEntry);
String imageFilename =
String.format(Locale.ROOT, "page_%03d.png", currentPage + 1);
zipOut.putNextEntry(new ZipEntry(imageFilename));
ImageIO.write(image, "PNG", zipOut);
zipOut.closeEntry();
ImageIO.write(image, "PNG", zipOut);
zipOut.closeEntry();
} catch (ExceptionUtils.OutOfMemoryDpiException e) {
// Re-throw OOM exceptions without wrapping
throw e;
} catch (IOException e) {
// Wrap other IOExceptions with context
throw ExceptionUtils.createFileProcessingException(
"CBZ creation for page " + (currentPage + 1), e);
} catch (ExceptionUtils.OutOfMemoryDpiException e) {
// Re-throw OOM exceptions without wrapping
throw e;
} catch (IOException e) {
// Wrap other IOExceptions with context
throw ExceptionUtils.createFileProcessingException(
"CBZ creation for page " + (currentPage + 1), e);
}
}
}
zipOut.finish();
return cbzOutputStream.toByteArray();
return cbzTempFile;
} catch (Exception e) {
cbzTempFile.close();
throw e;
}
}
@@ -219,58 +219,31 @@ public class PdfUtils {
int maxWidth = 0;
int totalHeight = 0;
BufferedImage pdfSizeImage = null;
int pdfSizeImageIndex = -1;
// Using a map to store the rendered dimensions of each page size
// to avoid rendering the same page sizes multiple times
// Using a map to store the calculated dimensions of each page size
HashMap<PdfRenderSettingsKey, PdfImageDimensionValue> pageSizes =
new HashMap<>();
for (int i = 0; i < pageCount; ++i) {
final int pageIndex = i;
PDPage page = document.getPage(i);
PDRectangle mediaBox = page.getMediaBox();
PDRectangle cropBox = page.getCropBox();
int rotation = page.getRotation();
PdfRenderSettingsKey settings =
new PdfRenderSettingsKey(
mediaBox.getWidth(), mediaBox.getHeight(), rotation);
cropBox.getWidth(), cropBox.getHeight(), rotation);
PdfImageDimensionValue dimension = pageSizes.get(settings);
if (dimension == null) {
// Render the image to get the dimensions
try {
// Validate dimensions before rendering
ExceptionUtils.validateRenderingDimensions(
page, pageIndex + 1, DPI);
pdfSizeImage =
ExceptionUtils.handleOomRendering(
pageIndex + 1,
DPI,
() ->
pdfRenderer.renderImageWithDPI(
pageIndex, DPI, colorType));
} catch (IllegalArgumentException e) {
if (e.getMessage() != null
&& e.getMessage()
.contains("Maximum size of image exceeded")) {
throw ExceptionUtils.createIllegalArgumentException(
"error.pageTooBigExceedsArray",
"PDF page {0} is too large to render at {1} DPI. The"
+ " resulting image would exceed Java's maximum"
+ " array size. Please try a lower DPI value"
+ " (recommended: 150 or less).",
i + 1,
DPI);
}
throw e;
float scale = DPI / 72f;
int widthPx = (int) Math.max(Math.floor(cropBox.getWidth() * scale), 1);
int heightPx =
(int) Math.max(Math.floor(cropBox.getHeight() * scale), 1);
if (rotation == 90 || rotation == 270) {
int tmp = widthPx;
widthPx = heightPx;
heightPx = tmp;
}
pdfSizeImageIndex = i;
dimension =
new PdfImageDimensionValue(
pdfSizeImage.getWidth(), pdfSizeImage.getHeight());
dimension = new PdfImageDimensionValue(widthPx, heightPx);
pageSizes.put(settings, dimension);
if (pdfSizeImage.getWidth() > maxWidth) {
maxWidth = pdfSizeImage.getWidth();
if (widthPx > maxWidth) {
maxWidth = widthPx;
}
}
totalHeight += dimension.height();
@@ -284,40 +257,32 @@ public class PdfUtils {
int currentHeight = 0;
BufferedImage pageImage;
// Check if the first image is the last rendered image
boolean firstImageAlreadyRendered = pdfSizeImageIndex == 0;
for (int i = 0; i < pageCount; ++i) {
final int pageIndex = i;
if (firstImageAlreadyRendered && i == 0) {
pageImage = pdfSizeImage;
} else {
try {
// Validate dimensions before rendering
ExceptionUtils.validateRenderingDimensions(
document.getPage(pageIndex), pageIndex + 1, DPI);
try {
// Validate dimensions before rendering
ExceptionUtils.validateRenderingDimensions(
document.getPage(pageIndex), pageIndex + 1, DPI);
pageImage =
ExceptionUtils.handleOomRendering(
pageIndex + 1,
DPI,
() ->
pdfRenderer.renderImageWithDPI(
pageIndex, DPI, colorType));
} catch (IllegalArgumentException e) {
if (e.getMessage() != null
&& e.getMessage()
.contains("Maximum size of image exceeded")) {
throw ExceptionUtils.createIllegalArgumentException(
"error.pageTooBigForDpi",
"PDF page {0} is too large to render at {1} DPI. Please"
+ " try a lower DPI value (recommended: 150 or"
+ " less).",
i + 1,
DPI);
}
throw e;
pageImage =
ExceptionUtils.handleOomRendering(
pageIndex + 1,
DPI,
() ->
pdfRenderer.renderImageWithDPI(
pageIndex, DPI, colorType));
} catch (IllegalArgumentException e) {
if (e.getMessage() != null
&& e.getMessage().contains("Maximum size of image exceeded")) {
throw ExceptionUtils.createIllegalArgumentException(
"error.pageTooBigForDpi",
"PDF page {0} is too large to render at {1} DPI. Please"
+ " try a lower DPI value (recommended: 150 or"
+ " less).",
i + 1,
DPI);
}
throw e;
}
// Calculate the x-coordinate to center the image
@@ -574,34 +539,39 @@ public class PdfUtils {
boolean everyPage)
throws IOException {
PDDocument document = pdfDocumentFactory.load(pdfBytes);
// Get the first page of the PDF
int pages = document.getNumberOfPages();
for (int i = 0; i < pages; i++) {
PDPage page = document.getPage(i);
try (PDPageContentStream contentStream =
new PDPageContentStream(
document, page, PDPageContentStream.AppendMode.APPEND, true, true)) {
// Create an image object from the image bytes
PDImageXObject image = PDImageXObject.createFromByteArray(document, imageBytes, "");
// Draw the image onto the page at the specified x and y coordinates
contentStream.drawImage(image, x, y);
log.info("Image successfully overlaid onto PDF");
if (!everyPage && i == 0) {
break;
try (PDDocument document = pdfDocumentFactory.load(pdfBytes)) {
// Get the first page of the PDF
int pages = document.getNumberOfPages();
for (int i = 0; i < pages; i++) {
PDPage page = document.getPage(i);
try (PDPageContentStream contentStream =
new PDPageContentStream(
document,
page,
PDPageContentStream.AppendMode.APPEND,
true,
true)) {
// Create an image object from the image bytes
PDImageXObject image =
PDImageXObject.createFromByteArray(document, imageBytes, "");
// Draw the image onto the page at the specified x and y coordinates
contentStream.drawImage(image, x, y);
log.info("Image successfully overlaid onto PDF");
if (!everyPage && i == 0) {
break;
}
} catch (IOException e) {
// Log an error message if there is an issue overlaying the image onto the PDF
log.error("Error overlaying image onto PDF", e);
throw e;
}
} catch (IOException e) {
// Log an error message if there is an issue overlaying the image onto the PDF
log.error("Error overlaying image onto PDF", e);
throw e;
}
// Create a ByteArrayOutputStream to save the PDF to
ByteArrayOutputStream baos = new ByteArrayOutputStream();
document.save(baos);
log.info("PDF successfully saved to byte array");
return baos.toByteArray();
}
// Create a ByteArrayOutputStream to save the PDF to
ByteArrayOutputStream baos = new ByteArrayOutputStream();
document.save(baos);
log.info("PDF successfully saved to byte array");
return baos.toByteArray();
}
public boolean containsTextInFile(PDDocument pdfDocument, String text, String pagesToCheck)
@@ -226,50 +226,54 @@ public class ProcessExecutor {
List<String> outputLines = new ArrayList<>();
Thread errorReaderThread =
new Thread(
() -> {
try (BufferedReader errorReader =
new BufferedReader(
new InputStreamReader(
process.getErrorStream(),
StandardCharsets.UTF_8))) {
String line;
while ((line =
BoundedLineReader.readLine(
errorReader, 5_000_000))
!= null) {
errorLines.add(line);
if (liveUpdates) log.info(line);
}
} catch (InterruptedIOException e) {
log.warn("Error reader thread was interrupted due to timeout.");
} catch (IOException e) {
log.error("exception", e);
}
});
Thread.ofVirtual()
.unstarted(
() -> {
try (BufferedReader errorReader =
new BufferedReader(
new InputStreamReader(
process.getErrorStream(),
StandardCharsets.UTF_8))) {
String line;
while ((line =
BoundedLineReader.readLine(
errorReader, 5_000_000))
!= null) {
errorLines.add(line);
if (liveUpdates) log.info(line);
}
} catch (InterruptedIOException e) {
log.warn(
"Error reader thread was interrupted due to timeout.");
} catch (IOException e) {
log.error("exception", e);
}
});
Thread outputReaderThread =
new Thread(
() -> {
try (BufferedReader outputReader =
new BufferedReader(
new InputStreamReader(
process.getInputStream(),
StandardCharsets.UTF_8))) {
String line;
while ((line =
BoundedLineReader.readLine(
outputReader, 5_000_000))
!= null) {
outputLines.add(line);
if (liveUpdates) log.info(line);
}
} catch (InterruptedIOException e) {
log.warn("Error reader thread was interrupted due to timeout.");
} catch (IOException e) {
log.error("exception", e);
}
});
Thread.ofVirtual()
.unstarted(
() -> {
try (BufferedReader outputReader =
new BufferedReader(
new InputStreamReader(
process.getInputStream(),
StandardCharsets.UTF_8))) {
String line;
while ((line =
BoundedLineReader.readLine(
outputReader, 5_000_000))
!= null) {
outputLines.add(line);
if (liveUpdates) log.info(line);
}
} catch (InterruptedIOException e) {
log.warn(
"Error reader thread was interrupted due to timeout.");
} catch (IOException e) {
log.error("exception", e);
}
});
errorReaderThread.start();
outputReaderThread.start();
@@ -370,7 +374,7 @@ public class ProcessExecutor {
}
// Match common unoconvert variants (but NOT soffice)
String lowerBasename = basename.toLowerCase(java.util.Locale.ROOT);
if (lowerBasename.contains("unoconvert") || lowerBasename.equals("unoconv")) {
if (lowerBasename.contains("unoconvert") || "unoconv".equals(lowerBasename)) {
return true;
}
}
@@ -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;
}
@@ -86,7 +86,6 @@ public class RequestUriUtils {
// Blocklist of backend/non-frontend paths that should still go through filters
String[] backendOnlyPrefixes = {
"/register",
"/invite",
"/pipeline",
"/pdfjs",
"/pdfjs-legacy",
@@ -173,6 +172,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,281 @@
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 static final Pattern NULL_BYTE_PATTERN = Pattern.compile("\u0000");
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://apache.org/xml/features/disallow-doctype-decl", 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 = NULL_BYTE_PATTERN.matcher(result).replaceAll("");
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;
}
}
@@ -191,7 +191,7 @@ public class YamlHelper {
mappingNode.getValue().clear();
mappingNode.getValue().addAll(updatedTuples);
}
setNewNode(node);
updatedRootNode = node;
return updated;
}
@@ -19,7 +19,7 @@ public abstract class ReplaceAndInvertColorStrategy extends PDFFile {
public ReplaceAndInvertColorStrategy(MultipartFile file, ReplaceAndInvert replaceAndInvert) {
setFileInput(file);
setReplaceAndInvert(replaceAndInvert);
this.replaceAndInvert = replaceAndInvert;
}
public abstract InputStreamResource replace() throws IOException;
@@ -4,18 +4,23 @@ import java.beans.PropertyEditorSupport;
import java.util.ArrayList;
import java.util.List;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import stirling.software.common.model.api.security.RedactionArea;
import tools.jackson.core.type.TypeReference;
import tools.jackson.databind.DeserializationFeature;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.json.JsonMapper;
@Slf4j
public class StringToArrayListPropertyEditor extends PropertyEditorSupport {
private final ObjectMapper objectMapper = new ObjectMapper();
private final ObjectMapper objectMapper =
JsonMapper.builder()
.enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.build();
@Override
public void setAsText(String text) throws IllegalArgumentException {
@@ -24,7 +29,6 @@ public class StringToArrayListPropertyEditor extends PropertyEditorSupport {
return;
}
try {
objectMapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true);
TypeReference<ArrayList<RedactionArea>> typeRef = new TypeReference<>() {};
List<RedactionArea> list = objectMapper.readValue(text, typeRef);
setValue(list);
@@ -4,12 +4,13 @@ import java.beans.PropertyEditorSupport;
import java.util.HashMap;
import java.util.Map;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import tools.jackson.core.type.TypeReference;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.json.JsonMapper;
public class StringToMapPropertyEditor extends PropertyEditorSupport {
private final ObjectMapper objectMapper = new ObjectMapper();
private final ObjectMapper objectMapper = JsonMapper.builder().build();
@Override
public void setAsText(String text) throws IllegalArgumentException {
@@ -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());
}
}
*/
@@ -12,11 +12,12 @@ class SpyPDFDocumentFactory extends CustomPDFDocumentFactory {
public StrategyType lastStrategyUsed;
public SpyPDFDocumentFactory(PdfMetadataService service) {
super(service);
super(service); // TempFileManager falls back to Files.createTempFile in tests
}
@Override
public StreamCacheCreateFunction getStreamCacheFunction(long contentSize) {
protected StreamCacheCreateFunction getStreamCacheFunction(
long contentSize, MemorySnapshot mem) {
StrategyType type;
if (contentSize < 10 * 1024 * 1024) {
type = StrategyType.MEMORY_ONLY;
@@ -26,6 +27,6 @@ class SpyPDFDocumentFactory extends CustomPDFDocumentFactory {
type = StrategyType.TEMP_FILE;
}
this.lastStrategyUsed = type;
return super.getStreamCacheFunction(contentSize); // delegate to real behavior
return super.getStreamCacheFunction(contentSize, mem);
}
}
@@ -0,0 +1,56 @@
package stirling.software.common.util;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.ApplicationArguments;
class AppArgsCaptureTest {
private AppArgsCapture capture;
@BeforeEach
void setUp() {
capture = new AppArgsCapture();
AppArgsCapture.APP_ARGS.set(List.of());
}
@Test
void run_withArgs_capturesArgs() {
ApplicationArguments args = mock(ApplicationArguments.class);
when(args.getSourceArgs()).thenReturn(new String[] {"--server.port=8080", "--debug"});
capture.run(args);
assertEquals(List.of("--server.port=8080", "--debug"), AppArgsCapture.APP_ARGS.get());
}
@Test
void run_withNoArgs_capturesEmptyList() {
ApplicationArguments args = mock(ApplicationArguments.class);
when(args.getSourceArgs()).thenReturn(new String[] {});
capture.run(args);
assertEquals(List.of(), AppArgsCapture.APP_ARGS.get());
}
@Test
void run_calledTwice_overwritesPreviousArgs() {
ApplicationArguments args1 = mock(ApplicationArguments.class);
when(args1.getSourceArgs()).thenReturn(new String[] {"--first"});
capture.run(args1);
assertEquals(List.of("--first"), AppArgsCapture.APP_ARGS.get());
ApplicationArguments args2 = mock(ApplicationArguments.class);
when(args2.getSourceArgs()).thenReturn(new String[] {"--second", "--third"});
capture.run(args2);
assertEquals(List.of("--second", "--third"), AppArgsCapture.APP_ARGS.get());
}
@Test
void appArgs_defaultValue_isEmptyList() {
// After setUp resets it
assertTrue(AppArgsCapture.APP_ARGS.get().isEmpty());
}
}
@@ -0,0 +1,108 @@
package stirling.software.common.util;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.ApplicationContext;
class ApplicationContextProviderTest {
private ApplicationContextProvider provider;
@BeforeEach
void setUp() {
provider = new ApplicationContextProvider();
// Reset to null state
provider.setApplicationContext(null);
}
@AfterEach
void tearDown() {
// Clean up static state
provider.setApplicationContext(null);
}
@Test
void getBean_byClass_whenNoContext_returnsNull() {
provider.setApplicationContext(null);
assertNull(ApplicationContextProvider.getBean(String.class));
}
@Test
void getBean_byClass_whenBeanExists_returnsBean() {
ApplicationContext ctx = mock(ApplicationContext.class);
when(ctx.getBean(String.class)).thenReturn("hello");
provider.setApplicationContext(ctx);
assertEquals("hello", ApplicationContextProvider.getBean(String.class));
}
@Test
void getBean_byClass_whenBeanNotFound_returnsNull() {
ApplicationContext ctx = mock(ApplicationContext.class);
when(ctx.getBean(String.class)).thenThrow(new NoSuchBeanDefinitionException(""));
provider.setApplicationContext(ctx);
assertNull(ApplicationContextProvider.getBean(String.class));
}
@Test
void getBean_byNameAndClass_whenNoContext_returnsNull() {
provider.setApplicationContext(null);
assertNull(ApplicationContextProvider.getBean("myBean", String.class));
}
@Test
void getBean_byNameAndClass_whenBeanExists_returnsBean() {
ApplicationContext ctx = mock(ApplicationContext.class);
when(ctx.getBean("myBean", String.class)).thenReturn("world");
provider.setApplicationContext(ctx);
assertEquals("world", ApplicationContextProvider.getBean("myBean", String.class));
}
@Test
void getBean_byNameAndClass_whenBeanNotFound_returnsNull() {
ApplicationContext ctx = mock(ApplicationContext.class);
when(ctx.getBean("missing", String.class)).thenThrow(new NoSuchBeanDefinitionException(""));
provider.setApplicationContext(ctx);
assertNull(ApplicationContextProvider.getBean("missing", String.class));
}
@Test
void containsBean_whenNoContext_returnsFalse() {
provider.setApplicationContext(null);
assertFalse(ApplicationContextProvider.containsBean(String.class));
}
@Test
void containsBean_whenBeanExists_returnsTrue() {
ApplicationContext ctx = mock(ApplicationContext.class);
when(ctx.getBean(String.class)).thenReturn("exists");
provider.setApplicationContext(ctx);
assertTrue(ApplicationContextProvider.containsBean(String.class));
}
@Test
void containsBean_whenBeanNotFound_returnsFalse() {
ApplicationContext ctx = mock(ApplicationContext.class);
when(ctx.getBean(Integer.class)).thenThrow(new NoSuchBeanDefinitionException(""));
provider.setApplicationContext(ctx);
assertFalse(ApplicationContextProvider.containsBean(Integer.class));
}
@Test
void setApplicationContext_updatesStaticContext() {
ApplicationContext ctx = mock(ApplicationContext.class);
when(ctx.getBean(String.class)).thenReturn("test");
provider.setApplicationContext(ctx);
assertEquals("test", ApplicationContextProvider.getBean(String.class));
// Now set a different context
ApplicationContext ctx2 = mock(ApplicationContext.class);
when(ctx2.getBean(String.class)).thenReturn("updated");
provider.setApplicationContext(ctx2);
assertEquals("updated", ApplicationContextProvider.getBean(String.class));
}
}
@@ -0,0 +1,70 @@
package stirling.software.common.util;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import org.apache.pdfbox.cos.COSDictionary;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDDocumentCatalog;
import org.apache.pdfbox.pdmodel.PageMode;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
class AttachmentUtilsTest {
@Test
@DisplayName("should set page mode on catalog")
void setsPageMode() {
try (PDDocument document = new PDDocument()) {
AttachmentUtils.setCatalogViewerPreferences(document, PageMode.USE_ATTACHMENTS);
PDDocumentCatalog catalog = document.getDocumentCatalog();
assertEquals(PageMode.USE_ATTACHMENTS, catalog.getPageMode());
} catch (Exception e) {
fail("Should not throw: " + e.getMessage());
}
}
@Test
@DisplayName("should create viewer preferences dictionary if absent")
void createsViewerPreferences() {
try (PDDocument document = new PDDocument()) {
AttachmentUtils.setCatalogViewerPreferences(document, PageMode.USE_ATTACHMENTS);
COSDictionary catalogDict = document.getDocumentCatalog().getCOSObject();
COSDictionary viewerPrefs =
(COSDictionary) catalogDict.getDictionaryObject(COSName.VIEWER_PREFERENCES);
assertNotNull(viewerPrefs);
} catch (Exception e) {
fail("Should not throw: " + e.getMessage());
}
}
@Test
@DisplayName("should set DisplayDocTitle to true in viewer preferences")
void setsDisplayDocTitle() {
try (PDDocument document = new PDDocument()) {
AttachmentUtils.setCatalogViewerPreferences(document, PageMode.USE_ATTACHMENTS);
COSDictionary catalogDict = document.getDocumentCatalog().getCOSObject();
COSDictionary viewerPrefs =
(COSDictionary) catalogDict.getDictionaryObject(COSName.VIEWER_PREFERENCES);
assertTrue(viewerPrefs.getBoolean(COSName.getPDFName("DisplayDocTitle"), false));
} catch (Exception e) {
fail("Should not throw: " + e.getMessage());
}
}
@Test
@DisplayName("should not throw when catalog returns null from mocked document")
void handlesNullCatalogGracefully() {
PDDocument document = mock(PDDocument.class);
when(document.getDocumentCatalog()).thenReturn(null);
assertDoesNotThrow(
() ->
AttachmentUtils.setCatalogViewerPreferences(
document, PageMode.USE_ATTACHMENTS));
}
}
@@ -0,0 +1,98 @@
package stirling.software.common.util;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import org.junit.jupiter.api.Test;
import org.springframework.web.multipart.MultipartFile;
class CbrUtilsTest {
// --- isCbrFile tests ---
@Test
void isCbrFile_withCbrExtension_returnsTrue() {
MultipartFile file = mock(MultipartFile.class);
when(file.getOriginalFilename()).thenReturn("comic.cbr");
assertTrue(CbrUtils.isCbrFile(file));
}
@Test
void isCbrFile_withRarExtension_returnsTrue() {
MultipartFile file = mock(MultipartFile.class);
when(file.getOriginalFilename()).thenReturn("archive.rar");
assertTrue(CbrUtils.isCbrFile(file));
}
@Test
void isCbrFile_withUpperCaseExtension_returnsTrue() {
MultipartFile file = mock(MultipartFile.class);
when(file.getOriginalFilename()).thenReturn("comic.CBR");
assertTrue(CbrUtils.isCbrFile(file));
}
@Test
void isCbrFile_withPdfExtension_returnsFalse() {
MultipartFile file = mock(MultipartFile.class);
when(file.getOriginalFilename()).thenReturn("document.pdf");
assertFalse(CbrUtils.isCbrFile(file));
}
@Test
void isCbrFile_withNullFilename_returnsFalse() {
MultipartFile file = mock(MultipartFile.class);
when(file.getOriginalFilename()).thenReturn(null);
assertFalse(CbrUtils.isCbrFile(file));
}
@Test
void isCbrFile_withCbzExtension_returnsFalse() {
MultipartFile file = mock(MultipartFile.class);
when(file.getOriginalFilename()).thenReturn("comic.cbz");
assertFalse(CbrUtils.isCbrFile(file));
}
@Test
void isCbrFile_withNoExtension_returnsFalse() {
MultipartFile file = mock(MultipartFile.class);
when(file.getOriginalFilename()).thenReturn("noextension");
assertFalse(CbrUtils.isCbrFile(file));
}
@Test
void isCbrFile_withMixedCaseRar_returnsTrue() {
MultipartFile file = mock(MultipartFile.class);
when(file.getOriginalFilename()).thenReturn("file.RaR");
assertTrue(CbrUtils.isCbrFile(file));
}
// --- convertCbrToPdf validation tests ---
@Test
void convertCbrToPdf_withNullFile_throwsException() {
assertThrows(Exception.class, () -> CbrUtils.convertCbrToPdf(null, null, null));
}
@Test
void convertCbrToPdf_withEmptyFile_throwsException() {
MultipartFile file = mock(MultipartFile.class);
when(file.isEmpty()).thenReturn(true);
assertThrows(Exception.class, () -> CbrUtils.convertCbrToPdf(file, null, null));
}
@Test
void convertCbrToPdf_withNullFilename_throwsException() {
MultipartFile file = mock(MultipartFile.class);
when(file.isEmpty()).thenReturn(false);
when(file.getOriginalFilename()).thenReturn(null);
assertThrows(Exception.class, () -> CbrUtils.convertCbrToPdf(file, null, null));
}
@Test
void convertCbrToPdf_withWrongExtension_throwsException() {
MultipartFile file = mock(MultipartFile.class);
when(file.isEmpty()).thenReturn(false);
when(file.getOriginalFilename()).thenReturn("file.pdf");
assertThrows(Exception.class, () -> CbrUtils.convertCbrToPdf(file, null, null));
}
}
@@ -0,0 +1,142 @@
package stirling.software.common.util;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import org.junit.jupiter.api.Test;
import org.springframework.web.multipart.MultipartFile;
class CbzUtilsTest {
// --- isCbzFile tests ---
@Test
void isCbzFile_withCbzExtension_returnsTrue() {
MultipartFile file = mock(MultipartFile.class);
when(file.getOriginalFilename()).thenReturn("comic.cbz");
assertTrue(CbzUtils.isCbzFile(file));
}
@Test
void isCbzFile_withZipExtension_returnsTrue() {
MultipartFile file = mock(MultipartFile.class);
when(file.getOriginalFilename()).thenReturn("archive.zip");
assertTrue(CbzUtils.isCbzFile(file));
}
@Test
void isCbzFile_withUpperCaseExtension_returnsTrue() {
MultipartFile file = mock(MultipartFile.class);
when(file.getOriginalFilename()).thenReturn("comic.CBZ");
assertTrue(CbzUtils.isCbzFile(file));
}
@Test
void isCbzFile_withPdfExtension_returnsFalse() {
MultipartFile file = mock(MultipartFile.class);
when(file.getOriginalFilename()).thenReturn("document.pdf");
assertFalse(CbzUtils.isCbzFile(file));
}
@Test
void isCbzFile_withNullFilename_returnsFalse() {
MultipartFile file = mock(MultipartFile.class);
when(file.getOriginalFilename()).thenReturn(null);
assertFalse(CbzUtils.isCbzFile(file));
}
@Test
void isCbzFile_withCbrExtension_returnsFalse() {
MultipartFile file = mock(MultipartFile.class);
when(file.getOriginalFilename()).thenReturn("comic.cbr");
assertFalse(CbzUtils.isCbzFile(file));
}
// --- isComicBookFile tests ---
@Test
void isComicBookFile_withCbzExtension_returnsTrue() {
MultipartFile file = mock(MultipartFile.class);
when(file.getOriginalFilename()).thenReturn("comic.cbz");
assertTrue(CbzUtils.isComicBookFile(file));
}
@Test
void isComicBookFile_withZipExtension_returnsTrue() {
MultipartFile file = mock(MultipartFile.class);
when(file.getOriginalFilename()).thenReturn("archive.zip");
assertTrue(CbzUtils.isComicBookFile(file));
}
@Test
void isComicBookFile_withCbrExtension_returnsTrue() {
MultipartFile file = mock(MultipartFile.class);
when(file.getOriginalFilename()).thenReturn("comic.cbr");
assertTrue(CbzUtils.isComicBookFile(file));
}
@Test
void isComicBookFile_withRarExtension_returnsTrue() {
MultipartFile file = mock(MultipartFile.class);
when(file.getOriginalFilename()).thenReturn("archive.rar");
assertTrue(CbzUtils.isComicBookFile(file));
}
@Test
void isComicBookFile_withPdfExtension_returnsFalse() {
MultipartFile file = mock(MultipartFile.class);
when(file.getOriginalFilename()).thenReturn("document.pdf");
assertFalse(CbzUtils.isComicBookFile(file));
}
@Test
void isComicBookFile_withNullFilename_returnsFalse() {
MultipartFile file = mock(MultipartFile.class);
when(file.getOriginalFilename()).thenReturn(null);
assertFalse(CbzUtils.isComicBookFile(file));
}
@Test
void isComicBookFile_withUpperCaseCBR_returnsTrue() {
MultipartFile file = mock(MultipartFile.class);
when(file.getOriginalFilename()).thenReturn("comic.CBR");
assertTrue(CbzUtils.isComicBookFile(file));
}
@Test
void isComicBookFile_withNoExtension_returnsFalse() {
MultipartFile file = mock(MultipartFile.class);
when(file.getOriginalFilename()).thenReturn("noextension");
assertFalse(CbzUtils.isComicBookFile(file));
}
// --- convertCbzToPdf validation tests ---
@Test
void convertCbzToPdf_withNullFile_throwsException() {
assertThrows(Exception.class, () -> CbzUtils.convertCbzToPdf(null, null, null, false));
}
@Test
void convertCbzToPdf_withEmptyFile_throwsException() {
MultipartFile file = mock(MultipartFile.class);
when(file.isEmpty()).thenReturn(true);
assertThrows(Exception.class, () -> CbzUtils.convertCbzToPdf(file, null, null, false));
}
@Test
void convertCbzToPdf_withNullFilename_throwsException() {
MultipartFile file = mock(MultipartFile.class);
when(file.isEmpty()).thenReturn(false);
when(file.getOriginalFilename()).thenReturn(null);
assertThrows(Exception.class, () -> CbzUtils.convertCbzToPdf(file, null, null, false));
}
@Test
void convertCbzToPdf_withWrongExtension_throwsException() {
MultipartFile file = mock(MultipartFile.class);
when(file.isEmpty()).thenReturn(false);
when(file.getOriginalFilename()).thenReturn("file.pdf");
assertThrows(Exception.class, () -> CbzUtils.convertCbzToPdf(file, null, null, false));
}
}
@@ -0,0 +1,210 @@
package stirling.software.common.util;
import static org.junit.jupiter.api.Assertions.*;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
class ChecksumUtilsAdditionalTest {
private static final byte[] HELLO = "hello".getBytes(StandardCharsets.UTF_8);
@TempDir Path tempDir;
private Path writeFile(byte[] data) throws IOException {
Path file = tempDir.resolve("testfile.bin");
Files.write(file, data);
return file;
}
// --- checksum(Path, String) ---
@Test
void testChecksumPath_sha256() throws IOException {
Path file = writeFile(HELLO);
String hex = ChecksumUtils.checksum(file, "SHA-256");
assertEquals("2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824", hex);
}
@Test
void testChecksumPath_md5() throws IOException {
Path file = writeFile(HELLO);
String hex = ChecksumUtils.checksum(file, "MD5");
assertEquals("5d41402abc4b2a76b9719d911017c592", hex);
}
@Test
void testChecksumPath_crc32() throws IOException {
Path file = writeFile(HELLO);
String hex = ChecksumUtils.checksum(file, "CRC32");
assertEquals("3610a686", hex);
}
// --- checksum(InputStream, String) ---
@Test
void testChecksumStream_adler32() throws IOException {
try (InputStream is = new ByteArrayInputStream(HELLO)) {
String hex = ChecksumUtils.checksum(is, "ADLER32");
assertNotNull(hex);
assertEquals(8, hex.length());
}
}
@Test
void testChecksumStream_sha1() throws IOException {
try (InputStream is = new ByteArrayInputStream(HELLO)) {
String hex = ChecksumUtils.checksum(is, "SHA-1");
assertEquals("aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d", hex);
}
}
@Test
void testChecksumStream_unsupportedAlgorithm() {
assertThrows(
IllegalStateException.class,
() -> {
try (InputStream is = new ByteArrayInputStream(HELLO)) {
ChecksumUtils.checksum(is, "FAKE-ALGO");
}
});
}
// --- checksumBase64(Path, String) ---
@Test
void testChecksumBase64Path_md5() throws IOException {
Path file = writeFile(HELLO);
String b64 = ChecksumUtils.checksumBase64(file, "MD5");
assertEquals("XUFAKrxLKna5cZ2REBfFkg==", b64);
}
@Test
void testChecksumBase64Path_crc32() throws IOException {
Path file = writeFile(HELLO);
String b64 = ChecksumUtils.checksumBase64(file, "CRC32");
assertEquals("NhCmhg==", b64);
}
// --- checksumBase64(InputStream, String) ---
@Test
void testChecksumBase64Stream_adler32() throws IOException {
try (InputStream is = new ByteArrayInputStream(HELLO)) {
String b64 = ChecksumUtils.checksumBase64(is, "ADLER32");
assertNotNull(b64);
assertFalse(b64.isEmpty());
}
}
@Test
void testChecksumBase64Stream_sha256() throws IOException {
try (InputStream is = new ByteArrayInputStream(HELLO)) {
String b64 = ChecksumUtils.checksumBase64(is, "SHA-256");
assertNotNull(b64);
assertFalse(b64.isEmpty());
}
}
// --- checksums(Path, String...) ---
@Test
void testChecksumsPath_multipleAlgorithms() throws IOException {
Path file = writeFile(HELLO);
Map<String, String> results = ChecksumUtils.checksums(file, "MD5", "SHA-256", "CRC32");
assertEquals(3, results.size());
assertEquals("5d41402abc4b2a76b9719d911017c592", results.get("MD5"));
assertEquals(
"2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824",
results.get("SHA-256"));
assertEquals("3610a686", results.get("CRC32"));
}
@Test
void testChecksumsPath_preservesOrder() throws IOException {
Path file = writeFile(HELLO);
// Digests are output first, then Checksums (CRC32/ADLER32), per implementation
Map<String, String> results = ChecksumUtils.checksums(file, "MD5", "SHA-1");
String[] keys = results.keySet().toArray(new String[0]);
assertEquals("MD5", keys[0]);
assertEquals("SHA-1", keys[1]);
}
@Test
void testChecksumsStream_unsupportedAlgorithm() {
assertThrows(
IllegalStateException.class,
() -> {
try (InputStream is = new ByteArrayInputStream(HELLO)) {
ChecksumUtils.checksums(is, "BOGUS");
}
});
}
// --- matches(Path, String, String) ---
@Test
void testMatchesPath_correctHash() throws IOException {
Path file = writeFile(HELLO);
assertTrue(ChecksumUtils.matches(file, "MD5", "5d41402abc4b2a76b9719d911017c592"));
}
@Test
void testMatchesPath_wrongHash() throws IOException {
Path file = writeFile(HELLO);
assertFalse(ChecksumUtils.matches(file, "MD5", "0000000000000000000000000000000000"));
}
@Test
void testMatchesPath_caseInsensitive() throws IOException {
Path file = writeFile(HELLO);
assertTrue(ChecksumUtils.matches(file, "MD5", "5D41402ABC4B2A76B9719D911017C592"));
}
// --- matches(InputStream, String, String) ---
@Test
void testMatchesStream_correct() throws IOException {
try (InputStream is = new ByteArrayInputStream(HELLO)) {
assertTrue(
ChecksumUtils.matches(is, "SHA-1", "aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d"));
}
}
@Test
void testMatchesStream_wrong() throws IOException {
try (InputStream is = new ByteArrayInputStream(HELLO)) {
assertFalse(
ChecksumUtils.matches(is, "SHA-1", "0000000000000000000000000000000000000000"));
}
}
// --- empty input ---
@Test
void testChecksumEmptyInput() throws IOException {
byte[] empty = new byte[0];
try (InputStream is = new ByteArrayInputStream(empty)) {
String hex = ChecksumUtils.checksum(is, "MD5");
// MD5 of empty input is d41d8cd98f00b204e9800998ecf8427e
assertEquals("d41d8cd98f00b204e9800998ecf8427e", hex);
}
}
@Test
void testChecksumCrc32EmptyInput() throws IOException {
byte[] empty = new byte[0];
try (InputStream is = new ByteArrayInputStream(empty)) {
String hex = ChecksumUtils.checksum(is, "CRC32");
assertEquals("00000000", hex);
}
}
}
@@ -0,0 +1,93 @@
package stirling.software.common.util;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
class EmlParserTest {
@Nested
@DisplayName("safeMimeDecode")
class SafeMimeDecodeTests {
@Test
@DisplayName("should return empty string for null input")
void nullInput() {
assertEquals("", EmlParser.safeMimeDecode(null));
}
@Test
@DisplayName("should return empty string for empty input")
void emptyInput() {
assertEquals("", EmlParser.safeMimeDecode(""));
}
@Test
@DisplayName("should return empty string for blank input")
void blankInput() {
assertEquals("", EmlParser.safeMimeDecode(" "));
}
@Test
@DisplayName("should return plain text as-is")
void plainText() {
assertEquals("Hello World", EmlParser.safeMimeDecode("Hello World"));
}
@Test
@DisplayName("should trim surrounding whitespace")
void trimWhitespace() {
assertEquals("Hello", EmlParser.safeMimeDecode(" Hello "));
}
@Test
@DisplayName("should decode base64 MIME encoded word")
void decodeBase64MimeWord() {
// =?UTF-8?B?SGVsbG8=?= is Base64 for "Hello"
assertEquals("Hello", EmlParser.safeMimeDecode("=?UTF-8?B?SGVsbG8=?="));
}
@Test
@DisplayName("should decode quoted-printable MIME encoded word")
void decodeQpMimeWord() {
// =?UTF-8?Q?Hello_World?= where _ means space in Q encoding
assertEquals("Hello World", EmlParser.safeMimeDecode("=?UTF-8?Q?Hello_World?="));
}
@Test
@DisplayName("should handle mixed text and encoded words")
void mixedTextAndEncoded() {
String input = "Re: =?UTF-8?B?SGVsbG8=?= test";
String result = EmlParser.safeMimeDecode(input);
assertEquals("Re: Hello test", result);
}
}
@Nested
@DisplayName("extractEmailContent")
class ExtractEmailContentTests {
@Test
@DisplayName("should throw on null input")
void nullInput() {
assertThrows(Exception.class, () -> EmlParser.extractEmailContent(null, null, null));
}
@Test
@DisplayName("should throw on empty input")
void emptyInput() {
assertThrows(
Exception.class, () -> EmlParser.extractEmailContent(new byte[0], null, null));
}
@Test
@DisplayName("should throw on invalid content that is not EML or MSG")
void invalidContent() {
byte[] randomBytes = "This is not an email file at all.".getBytes();
assertThrows(
Exception.class, () -> EmlParser.extractEmailContent(randomBytes, null, null));
}
}
}
@@ -0,0 +1,293 @@
package stirling.software.common.util;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
class EmlProcessingUtilsTest {
@Nested
@DisplayName("validateEmlInput")
class ValidateEmlInputTests {
@Test
@DisplayName("should throw on null input")
void nullInput() {
assertThrows(Exception.class, () -> EmlProcessingUtils.validateEmlInput(null));
}
@Test
@DisplayName("should throw on empty input")
void emptyInput() {
assertThrows(Exception.class, () -> EmlProcessingUtils.validateEmlInput(new byte[0]));
}
@Test
@DisplayName("should throw on invalid format with insufficient headers")
void invalidFormat() {
byte[] data = "Hello, this is just random text without email headers.".getBytes();
assertThrows(Exception.class, () -> EmlProcessingUtils.validateEmlInput(data));
}
@Test
@DisplayName("should accept valid EML with multiple headers")
void validEml() {
String emlContent =
"From: sender@example.com\r\n"
+ "To: recipient@example.com\r\n"
+ "Subject: Test\r\n"
+ "Date: Mon, 1 Jan 2024 00:00:00 +0000\r\n"
+ "\r\n"
+ "Body text";
assertDoesNotThrow(() -> EmlProcessingUtils.validateEmlInput(emlContent.getBytes()));
}
}
@Nested
@DisplayName("isMsgFile")
class IsMsgFileTests {
@Test
@DisplayName("should return false for null")
void nullInput() {
assertFalse(EmlProcessingUtils.isMsgFile(null));
}
@Test
@DisplayName("should return false for short bytes")
void shortBytes() {
assertFalse(EmlProcessingUtils.isMsgFile(new byte[] {0x01, 0x02}));
}
@Test
@DisplayName("should return true for MSG magic bytes")
void msgMagicBytes() {
byte[] magic = {
(byte) 0xD0,
(byte) 0xCF,
(byte) 0x11,
(byte) 0xE0,
(byte) 0xA1,
(byte) 0xB1,
(byte) 0x1A,
(byte) 0xE1,
0x00,
0x00
};
assertTrue(EmlProcessingUtils.isMsgFile(magic));
}
@Test
@DisplayName("should return false for non-MSG bytes")
void nonMsgBytes() {
byte[] data = new byte[] {0x50, 0x4B, 0x03, 0x04, 0x00, 0x00, 0x00, 0x00};
assertFalse(EmlProcessingUtils.isMsgFile(data));
}
}
@Nested
@DisplayName("escapeHtml")
class EscapeHtmlTests {
@Test
@DisplayName("should return empty string for null")
void nullInput() {
assertEquals("", EmlProcessingUtils.escapeHtml(null));
}
@Test
@DisplayName("should escape all HTML special characters")
void escapeSpecialChars() {
String result = EmlProcessingUtils.escapeHtml("<div class=\"test\">'&'</div>");
assertEquals("&lt;div class=&quot;test&quot;&gt;&#39;&amp;&#39;&lt;/div&gt;", result);
}
@Test
@DisplayName("should not modify plain text")
void plainText() {
assertEquals("Hello World", EmlProcessingUtils.escapeHtml("Hello World"));
}
}
@Nested
@DisplayName("convertTextToHtml")
class ConvertTextToHtmlTests {
@Test
@DisplayName("should return empty string for null")
void nullInput() {
assertEquals("", EmlProcessingUtils.convertTextToHtml(null, null));
}
@Test
@DisplayName("should convert newlines to br tags")
void newlinesToBr() {
String result = EmlProcessingUtils.convertTextToHtml("Line1\nLine2", null);
assertTrue(result.contains("<br>"));
}
@Test
@DisplayName("should convert CRLF to br tags")
void crlfToBr() {
String result = EmlProcessingUtils.convertTextToHtml("Line1\r\nLine2", null);
assertTrue(result.contains("<br>"));
assertFalse(result.contains("\r"));
}
@Test
@DisplayName("should linkify URLs")
void linkifyUrls() {
String result =
EmlProcessingUtils.convertTextToHtml("Visit https://example.com today", null);
assertTrue(result.contains("<a href=\"https://example.com\""));
}
@Test
@DisplayName("should linkify email addresses")
void linkifyEmails() {
String result = EmlProcessingUtils.convertTextToHtml("Contact test@example.com", null);
assertTrue(result.contains("mailto:test@example.com"));
}
}
@Nested
@DisplayName("decodeMimeHeader")
class DecodeMimeHeaderTests {
@Test
@DisplayName("should return null for null input")
void nullInput() {
assertNull(EmlProcessingUtils.decodeMimeHeader(null));
}
@Test
@DisplayName("should return empty string for empty input")
void emptyInput() {
assertEquals("", EmlProcessingUtils.decodeMimeHeader(""));
}
@Test
@DisplayName("should return plain text unchanged")
void plainText() {
assertEquals("Hello World", EmlProcessingUtils.decodeMimeHeader("Hello World"));
}
@Test
@DisplayName("should decode Base64 encoded header")
void decodeBase64() {
// "Hello" in Base64
String result = EmlProcessingUtils.decodeMimeHeader("=?UTF-8?B?SGVsbG8=?=");
assertEquals("Hello", result);
}
@Test
@DisplayName("should decode quoted-printable encoded header")
void decodeQuotedPrintable() {
String result = EmlProcessingUtils.decodeMimeHeader("=?UTF-8?Q?Hello_World?=");
assertEquals("Hello World", result);
}
@Test
@DisplayName("should decode concatenated encoded words")
void decodeConcatenated() {
String input = "=?UTF-8?B?SGVs?= =?UTF-8?B?bG8=?=";
String result = EmlProcessingUtils.decodeMimeHeader(input);
assertEquals("Hello", result);
}
@Test
@DisplayName("should handle unknown encoding gracefully")
void unknownEncoding() {
String input = "=?UTF-8?X?unknown?=";
String result = EmlProcessingUtils.decodeMimeHeader(input);
assertEquals("=?UTF-8?X?unknown?=", result);
}
}
@Nested
@DisplayName("detectMimeType")
class DetectMimeTypeTests {
@Test
@DisplayName("should return existing MIME type if provided")
void existingMimeType() {
assertEquals(
"image/jpeg", EmlProcessingUtils.detectMimeType("photo.png", "image/jpeg"));
}
@Test
@DisplayName("should detect PNG from filename")
void detectPng() {
assertEquals("image/png", EmlProcessingUtils.detectMimeType("image.png", null));
}
@Test
@DisplayName("should detect JPEG from filename")
void detectJpeg() {
assertEquals("image/jpeg", EmlProcessingUtils.detectMimeType("photo.jpg", null));
}
@Test
@DisplayName("should default to image/png for unknown extension")
void defaultMimeType() {
assertEquals("image/png", EmlProcessingUtils.detectMimeType("file.xyz", null));
}
@Test
@DisplayName("should default to image/png for null filename and mime")
void nullFilenameAndMime() {
assertEquals("image/png", EmlProcessingUtils.detectMimeType(null, null));
}
}
@Nested
@DisplayName("sanitizeText")
class SanitizeTextTests {
@Test
@DisplayName("should escape HTML when no sanitizer provided")
void noSanitizer() {
String result = EmlProcessingUtils.sanitizeText("<script>", null);
assertEquals("&lt;script&gt;", result);
}
}
@Nested
@DisplayName("processEmailHtmlBody")
class ProcessEmailHtmlBodyTests {
@Test
@DisplayName("should return empty string for null body")
void nullBody() {
assertEquals("", EmlProcessingUtils.processEmailHtmlBody(null, null, null));
}
@Test
@DisplayName("should strip fixed position CSS")
void stripFixedPosition() {
String html = "<div style=\"position:fixed; top:0\">content</div>";
String result = EmlProcessingUtils.processEmailHtmlBody(html, null, null);
assertFalse(result.contains("position:fixed"));
}
}
@Nested
@DisplayName("decodeUrlEncoded")
class DecodeUrlEncodedTests {
@Test
@DisplayName("should decode URL-encoded string")
void decodeEncoded() {
assertEquals("hello world", EmlProcessingUtils.decodeUrlEncoded("hello%20world"));
}
@Test
@DisplayName("should return original on invalid encoding")
void invalidEncoding() {
String result = EmlProcessingUtils.decodeUrlEncoded("%ZZinvalid");
assertEquals("%ZZinvalid", result);
}
}
}
@@ -1,44 +1,114 @@
package stirling.software.common.util;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.ui.Model;
import org.springframework.web.servlet.ModelAndView;
public class ErrorUtilsTest {
class ErrorUtilsTest {
@Test
public void testExceptionToModel() {
// Create a mock Model
Model model = new org.springframework.ui.ExtendedModelMap();
@Nested
@DisplayName("exceptionToModel")
class ExceptionToModelTests {
// Create a test exception
Exception ex = new Exception("Test Exception");
@Test
@DisplayName("should add error message to model")
void addsErrorMessage() {
Model model = mock(Model.class);
Exception ex = new RuntimeException("test error");
// Call the method under test
Model resultModel = ErrorUtils.exceptionToModel(model, ex);
ErrorUtils.exceptionToModel(model, ex);
// Verify the result
assertNotNull(resultModel);
assertEquals("Test Exception", resultModel.getAttribute("errorMessage"));
assertNotNull(resultModel.getAttribute("stackTrace"));
verify(model).addAttribute("errorMessage", "test error");
}
@Test
@DisplayName("should add stack trace to model")
void addsStackTrace() {
Model model = mock(Model.class);
Exception ex = new RuntimeException("test error");
ErrorUtils.exceptionToModel(model, ex);
verify(model)
.addAttribute(
eq("stackTrace"),
argThat(
arg ->
arg instanceof String s
&& s.contains("RuntimeException")
&& s.contains("test error")));
}
@Test
@DisplayName("should return the same model instance")
void returnsSameModel() {
Model model = mock(Model.class);
Exception ex = new RuntimeException("test");
Model result = ErrorUtils.exceptionToModel(model, ex);
assertSame(model, result);
}
@Test
@DisplayName("should handle exception with null message")
void nullExceptionMessage() {
Model model = mock(Model.class);
Exception ex = new RuntimeException((String) null);
ErrorUtils.exceptionToModel(model, ex);
verify(model).addAttribute("errorMessage", null);
}
}
@Test
public void testExceptionToModelView() {
// Create a mock Model
Model model = new org.springframework.ui.ExtendedModelMap();
@Nested
@DisplayName("exceptionToModelView")
class ExceptionToModelViewTests {
// Create a test exception
Exception ex = new Exception("Test Exception");
@Test
@DisplayName("should create ModelAndView with error message")
void addsErrorMessage() {
Model model = mock(Model.class);
Exception ex = new RuntimeException("view error");
// Call the method under test
ModelAndView modelAndView = ErrorUtils.exceptionToModelView(model, ex);
ModelAndView result = ErrorUtils.exceptionToModelView(model, ex);
// Verify the result
assertNotNull(modelAndView);
assertEquals("Test Exception", modelAndView.getModel().get("errorMessage"));
assertNotNull(modelAndView.getModel().get("stackTrace"));
assertNotNull(result);
assertEquals("view error", result.getModel().get("errorMessage"));
}
@Test
@DisplayName("should create ModelAndView with stack trace")
void addsStackTrace() {
Model model = mock(Model.class);
Exception ex = new RuntimeException("view error");
ModelAndView result = ErrorUtils.exceptionToModelView(model, ex);
String stackTrace = (String) result.getModel().get("stackTrace");
assertNotNull(stackTrace);
assertTrue(stackTrace.contains("RuntimeException"));
assertTrue(stackTrace.contains("view error"));
}
@Test
@DisplayName("should handle nested exception")
void nestedException() {
Model model = mock(Model.class);
Exception cause = new IllegalArgumentException("root cause");
Exception ex = new RuntimeException("wrapper", cause);
ModelAndView result = ErrorUtils.exceptionToModelView(model, ex);
String stackTrace = (String) result.getModel().get("stackTrace");
assertTrue(stackTrace.contains("root cause"));
assertEquals("wrapper", result.getModel().get("errorMessage"));
}
}
}
@@ -0,0 +1,90 @@
package stirling.software.common.util;
import static org.junit.jupiter.api.Assertions.*;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
class ExecutorFactoryTest {
@Test
@DisplayName("newVirtualThreadExecutor should return non-null executor")
void virtualThreadExecutorNotNull() {
ExecutorService executor = ExecutorFactory.newVirtualThreadExecutor();
assertNotNull(executor);
executor.shutdown();
}
@Test
@DisplayName("newVirtualThreadExecutor should execute tasks")
void virtualThreadExecutorExecutesTasks() throws Exception {
ExecutorService executor = ExecutorFactory.newVirtualThreadExecutor();
AtomicBoolean ran = new AtomicBoolean(false);
CountDownLatch latch = new CountDownLatch(1);
executor.submit(
() -> {
ran.set(true);
latch.countDown();
});
assertTrue(latch.await(5, TimeUnit.SECONDS));
assertTrue(ran.get());
executor.shutdown();
}
@Test
@DisplayName("newVirtualThreadExecutor should run on virtual threads")
void virtualThreadExecutorUsesVirtualThreads() throws Exception {
ExecutorService executor = ExecutorFactory.newVirtualThreadExecutor();
AtomicReference<Boolean> isVirtual = new AtomicReference<>();
CountDownLatch latch = new CountDownLatch(1);
executor.submit(
() -> {
isVirtual.set(Thread.currentThread().isVirtual());
latch.countDown();
});
assertTrue(latch.await(5, TimeUnit.SECONDS));
assertTrue(isVirtual.get());
executor.shutdown();
}
@Test
@DisplayName("newSingleVirtualThreadScheduledExecutor should return non-null")
void scheduledExecutorNotNull() {
ScheduledExecutorService executor =
ExecutorFactory.newSingleVirtualThreadScheduledExecutor();
assertNotNull(executor);
executor.shutdown();
}
@Test
@DisplayName("newSingleVirtualThreadScheduledExecutor should execute scheduled tasks")
void scheduledExecutorExecutesTasks() throws Exception {
ScheduledExecutorService executor =
ExecutorFactory.newSingleVirtualThreadScheduledExecutor();
AtomicBoolean ran = new AtomicBoolean(false);
CountDownLatch latch = new CountDownLatch(1);
executor.schedule(
() -> {
ran.set(true);
latch.countDown();
},
10,
TimeUnit.MILLISECONDS);
assertTrue(latch.await(5, TimeUnit.SECONDS));
assertTrue(ran.get());
executor.shutdown();
}
}
@@ -1,192 +1,110 @@
package stirling.software.common.util;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.*;
import java.io.IOException;
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;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import stirling.software.common.configuration.RuntimePathConfig;
@ExtendWith(MockitoExtension.class)
class FileMonitorTest {
@TempDir Path tempDir;
@Mock private RuntimePathConfig runtimePathConfig;
@Mock private Predicate<Path> pathFilter;
private FileMonitor fileMonitor;
@BeforeEach
void setUp() throws IOException {
when(runtimePathConfig.getPipelineWatchedFoldersPath()).thenReturn(tempDir.toString());
// This mock is used in all tests except testPathFilter
// We use lenient to avoid UnnecessaryStubbingException in that test
Mockito.lenient().when(pathFilter.test(any())).thenReturn(true);
fileMonitor = new FileMonitor(pathFilter, runtimePathConfig);
private FileMonitor createFileMonitor(Path watchDir) throws IOException {
Predicate<Path> acceptAll = path -> true;
RuntimePathConfig runtimePathConfig = mock(RuntimePathConfig.class);
when(runtimePathConfig.getPipelineWatchedFoldersPaths())
.thenReturn(List.of(watchDir.toString()));
return new FileMonitor(acceptAll, runtimePathConfig);
}
@Test
void testIsFileReadyForProcessing_OldFile() throws IOException {
// Capture test time at the beginning for deterministic calculations
final Instant testTime = Instant.now();
// Create a test file
Path testFile = tempDir.resolve("test-file.txt");
Files.write(testFile, "test content".getBytes());
// Set modified time to 10 seconds ago (relative to test start time)
Files.setLastModifiedTime(testFile, FileTime.from(testTime.minusMillis(10000)));
// File should be ready for processing as it was modified more than 5 seconds ago
assertTrue(fileMonitor.isFileReadyForProcessing(testFile));
void testConstructor_withValidDirectory() throws IOException {
FileMonitor monitor = createFileMonitor(tempDir);
assertNotNull(monitor);
}
@Test
void testIsFileReadyForProcessing_RecentFile() throws IOException {
// Capture test time at the beginning for deterministic calculations
final Instant testTime = Instant.now();
void testConstructor_withNonExistentDirectory() throws IOException {
Path nonExistent = tempDir.resolve("does_not_exist");
Predicate<Path> acceptAll = path -> true;
RuntimePathConfig config = mock(RuntimePathConfig.class);
when(config.getPipelineWatchedFoldersPaths()).thenReturn(List.of(nonExistent.toString()));
// Create a test file
Path testFile = tempDir.resolve("recent-file.txt");
Files.write(testFile, "test content".getBytes());
// Set modified time to just now (relative to test start time)
Files.setLastModifiedTime(testFile, FileTime.from(testTime));
// File should not be ready for processing as it was just modified
assertFalse(fileMonitor.isFileReadyForProcessing(testFile));
// Should not throw - just logs an error about non-existent path
FileMonitor monitor = new FileMonitor(acceptAll, config);
assertNotNull(monitor);
}
@Test
void testIsFileReadyForProcessing_NonExistentFile() {
// Create a path to a file that doesn't exist
Path nonExistentFile = tempDir.resolve("non-existent-file.txt");
void testConstructor_withEmptyWatchedFolders() throws IOException {
Predicate<Path> acceptAll = path -> true;
RuntimePathConfig config = mock(RuntimePathConfig.class);
when(config.getPipelineWatchedFoldersPaths()).thenReturn(List.of());
// Non-existent file should not be ready for processing
assertFalse(fileMonitor.isFileReadyForProcessing(nonExistentFile));
FileMonitor monitor = new FileMonitor(acceptAll, config);
assertNotNull(monitor);
}
@Test
void testIsFileReadyForProcessing_LockedFile() throws IOException {
// Capture test time at the beginning for deterministic calculations
final Instant testTime = Instant.now();
// Create a test file
Path testFile = tempDir.resolve("locked-file.txt");
Files.write(testFile, "test content".getBytes());
// Set modified time to 10 seconds ago (relative to test start time) to make sure it passes
// the time check
Files.setLastModifiedTime(testFile, FileTime.from(testTime.minusMillis(10000)));
// Verify the file is considered ready when it meets the time criteria
assertTrue(
fileMonitor.isFileReadyForProcessing(testFile),
"File should be ready for processing when sufficiently old");
void testTrackFiles_noEventsDoesNotThrow() throws IOException {
FileMonitor monitor = createFileMonitor(tempDir);
// Should not throw even when no events have occurred
assertDoesNotThrow(() -> monitor.trackFiles());
}
@Test
void testPathFilter() throws IOException {
// Use a simple lambda instead of a mock for better control
Predicate<Path> pdfFilter = path -> path.toString().endsWith(".pdf");
void testIsFileReadyForProcessing_nonExistentFile() throws IOException {
FileMonitor monitor = createFileMonitor(tempDir);
Path nonExistent = tempDir.resolve("nonexistent.pdf");
// Create a new FileMonitor with the PDF filter
FileMonitor pdfMonitor = new FileMonitor(pdfFilter, runtimePathConfig);
// Create a PDF file
Path pdfFile = tempDir.resolve("test.pdf");
Files.write(pdfFile, "pdf content".getBytes());
Files.setLastModifiedTime(pdfFile, FileTime.from(Instant.ofEpochMilli(1000000L)));
// Create a TXT file
Path txtFile = tempDir.resolve("test.txt");
Files.write(txtFile, "text content".getBytes());
Files.setLastModifiedTime(txtFile, FileTime.from(Instant.ofEpochMilli(1000000L)));
// PDF file should be ready for processing
assertTrue(pdfMonitor.isFileReadyForProcessing(pdfFile));
// Note: In the current implementation, FileMonitor.isFileReadyForProcessing()
// doesn't check file filters directly - it only checks criteria like file existence
// and modification time. The filtering is likely handled elsewhere in the workflow.
// To avoid test failures, we'll verify that the filter itself works correctly
assertFalse(pdfFilter.test(txtFile), "PDF filter should reject txt files");
assertTrue(pdfFilter.test(pdfFile), "PDF filter should accept pdf files");
// Non-existent file should not be ready (file lock check will fail)
boolean ready = monitor.isFileReadyForProcessing(nonExistent);
assertFalse(ready, "Non-existent file should not be ready for processing");
}
@Test
void testIsFileReadyForProcessing_FileInUse() throws IOException {
// Capture test time at the beginning for deterministic calculations
final Instant testTime = Instant.now();
void testIsFileReadyForProcessing_existingFile() throws IOException, InterruptedException {
FileMonitor monitor = createFileMonitor(tempDir);
Path testFile = tempDir.resolve("test.pdf");
Files.writeString(testFile, "test content");
// Create a test file
Path testFile = tempDir.resolve("in-use-file.txt");
Files.write(testFile, "initial content".getBytes());
// Run trackFiles to process any events
monitor.trackFiles();
// Set modified time to 10 seconds ago (relative to test start time)
Files.setLastModifiedTime(testFile, FileTime.from(testTime.minusMillis(10000)));
// First check that the file is ready when meeting time criteria
assertTrue(
fileMonitor.isFileReadyForProcessing(testFile),
"File should be ready for processing when sufficiently old");
// After modifying the file to simulate closing, it should still be ready
Files.write(testFile, "updated content".getBytes());
Files.setLastModifiedTime(testFile, FileTime.from(testTime.minusMillis(10000)));
assertTrue(
fileMonitor.isFileReadyForProcessing(testFile),
"File should be ready for processing after updating");
// The file might or might not be ready depending on timing,
// but calling the method should not throw
assertDoesNotThrow(() -> monitor.isFileReadyForProcessing(testFile));
}
@Test
void testIsFileReadyForProcessing_FileWithAbsolutePath() throws IOException {
// Capture test time at the beginning for deterministic calculations
final Instant testTime = Instant.now();
void testTrackFiles_afterFileCreation() throws IOException {
FileMonitor monitor = createFileMonitor(tempDir);
// Create a test file
Path testFile = tempDir.resolve("absolute-path-file.txt");
Files.write(testFile, "test content".getBytes());
// Create a file in the watched directory
Path testFile = tempDir.resolve("newfile.txt");
Files.writeString(testFile, "hello");
// Set modified time to 10 seconds ago (relative to test start time)
Files.setLastModifiedTime(testFile, FileTime.from(testTime.minusMillis(10000)));
// File should be ready for processing as it was modified more than 5 seconds ago
// Use the absolute path to make sure it's handled correctly
assertTrue(fileMonitor.isFileReadyForProcessing(testFile.toAbsolutePath()));
// Track files should process the creation event
assertDoesNotThrow(() -> monitor.trackFiles());
}
@Test
void testIsFileReadyForProcessing_DirectoryInsteadOfFile() throws IOException {
// Create a test directory
Path testDir = tempDir.resolve("test-directory");
Files.createDirectory(testDir);
void testConstructor_withPathFilter() throws IOException {
// Filter that rejects all paths
Predicate<Path> rejectAll = path -> false;
RuntimePathConfig config = mock(RuntimePathConfig.class);
when(config.getPipelineWatchedFoldersPaths()).thenReturn(List.of(tempDir.toString()));
// Set modified time to 10 seconds ago
Files.setLastModifiedTime(testDir, FileTime.from(Instant.ofEpochMilli(1000000L)));
// A directory should not be considered ready for processing
boolean isReady = fileMonitor.isFileReadyForProcessing(testDir);
assertFalse(isReady, "A directory should not be considered ready for processing");
FileMonitor monitor = new FileMonitor(rejectAll, config);
assertNotNull(monitor);
}
}
@@ -0,0 +1,357 @@
package stirling.software.common.util;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Answers.CALLS_REAL_METHODS;
import static org.mockito.Mockito.*;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.FileTime;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.MockitoAnnotations;
import stirling.software.common.model.ApplicationProperties;
import stirling.software.common.model.ApplicationProperties.AutoPipeline.FileReadiness;
@DisplayName("FileReadinessChecker")
class FileReadinessCheckerTest {
@TempDir Path tempDir;
@Mock ApplicationProperties applicationProperties;
@Mock ApplicationProperties.AutoPipeline autoPipeline;
/** Real config object — easier to tweak per test than chaining multiple stubs. */
FileReadiness config;
FileReadinessChecker checker;
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
config = new FileReadiness();
config.setEnabled(true);
config.setSettleTimeMillis(0); // instant settle by default — individual tests override
config.setSizeCheckDelayMillis(1); // minimal pause keeps tests fast
config.setAllowedExtensions(new ArrayList<>());
when(applicationProperties.getAutoPipeline()).thenReturn(autoPipeline);
when(autoPipeline.getFileReadiness()).thenReturn(config);
checker = new FileReadinessChecker(applicationProperties);
}
// =========================================================================
// Master toggle
// =========================================================================
@Nested
@DisplayName("when enabled=false")
class WhenDisabled {
@Test
@DisplayName("always returns true regardless of file state")
void alwaysReady() throws IOException {
config.setEnabled(false);
// Non-existent path — would normally fail check #1
Path ghost = tempDir.resolve("does-not-exist.pdf");
assertTrue(checker.isReady(ghost));
}
}
// =========================================================================
// Check #1 + #2: existence and regular-file guard
// =========================================================================
@Nested
@DisplayName("existence and file-type checks")
class ExistenceChecks {
@Test
@DisplayName("non-existent path → not ready")
void fileDoesNotExist() {
Path ghost = tempDir.resolve("ghost.pdf");
assertFalse(checker.isReady(ghost));
}
@Test
@DisplayName("path is a directory → not ready")
void pathIsDirectory() throws IOException {
Path dir = tempDir.resolve("subdir");
Files.createDirectory(dir);
assertFalse(checker.isReady(dir));
}
@Test
@DisplayName("path is a regular file → passes existence checks")
void regularFilePassesExistenceCheck() throws IOException {
Path file = realFile("test.pdf", "content");
setLastModifiedInPast(file, 60_000);
assertTrue(checker.isReady(file));
}
}
// =========================================================================
// Check #3: extension filter
// =========================================================================
@Nested
@DisplayName("extension filter")
class ExtensionFilter {
@Test
@DisplayName("empty allow-list → all extensions accepted")
void emptyAllowListAcceptsAll() throws IOException {
config.setAllowedExtensions(new ArrayList<>()); // empty = no filter
Path file = realFile("report.docx", "data");
setLastModifiedInPast(file, 60_000);
assertTrue(checker.isReady(file));
}
@Test
@DisplayName("extension in allow-list → passes")
void extensionInAllowList() throws IOException {
config.setAllowedExtensions(List.of("pdf", "tiff"));
Path file = realFile("scan.pdf", "data");
setLastModifiedInPast(file, 60_000);
assertTrue(checker.isReady(file));
}
@Test
@DisplayName("extension not in allow-list → not ready")
void extensionNotInAllowList() throws IOException {
config.setAllowedExtensions(List.of("pdf", "tiff"));
Path file = realFile("document.docx", "data");
setLastModifiedInPast(file, 60_000);
assertFalse(checker.isReady(file));
}
@Test
@DisplayName("extension matching is case-insensitive")
void extensionMatchIsCaseInsensitive() throws IOException {
config.setAllowedExtensions(List.of("PDF"));
Path file = realFile("scan.pdf", "data");
setLastModifiedInPast(file, 60_000);
assertTrue(checker.isReady(file));
}
@Test
@DisplayName("file without extension and non-empty allow-list → not ready")
void fileWithNoExtension() throws IOException {
config.setAllowedExtensions(List.of("pdf"));
Path file = realFile("README", "data");
setLastModifiedInPast(file, 60_000);
assertFalse(checker.isReady(file));
}
}
// =========================================================================
// Check #4: settle-time (last-modified age)
// =========================================================================
@Nested
@DisplayName("settle-time check")
class SettleTime {
@Test
@DisplayName("recently modified file → not ready")
void recentlyModified_notReady() throws IOException {
config.setSettleTimeMillis(60_000); // require 1 minute of quiet
Path file = realFile("new.pdf", "data");
// last-modified is now (just created) — well within the threshold
assertFalse(checker.isReady(file));
}
@Test
@DisplayName("file settled for longer than threshold → ready")
void settled_ready() throws IOException {
config.setSettleTimeMillis(5_000);
Path file = realFile("old.pdf", "data");
setLastModifiedInPast(file, 10_000); // 10 s ago — older than 5 s threshold
assertTrue(checker.isReady(file));
}
@Test
@DisplayName("settle threshold of 0 ms passes any file")
void zeroThreshold_alwaysPasses() throws IOException {
config.setSettleTimeMillis(0);
Path file = realFile("instant.pdf", "data");
// last-modified is right now; 0 ms threshold means anything passes
assertTrue(checker.isReady(file));
}
}
// =========================================================================
// Check #5: size stability
// =========================================================================
@Nested
@DisplayName("size-stability check")
class SizeStability {
@Test
@DisplayName("size unchanged between two reads → ready")
void sizeStable_ready() throws IOException {
config.setSizeCheckDelayMillis(1);
Path file = realFile("stable.pdf", "fixed content");
setLastModifiedInPast(file, 60_000);
assertTrue(checker.isReady(file));
}
@Test
@DisplayName("size changes between two reads → not ready")
void sizeChanging_notReady() throws IOException {
config.setSizeCheckDelayMillis(1);
Path file = realFile("growing.pdf", "initial");
setLastModifiedInPast(file, 60_000);
// Use MockedStatic to control what Files.size() returns on each call
// while leaving all other Files.* methods intact.
AtomicInteger sizeCallCount = new AtomicInteger(0);
try (MockedStatic<Files> mockedFiles = mockStatic(Files.class, CALLS_REAL_METHODS)) {
mockedFiles
.when(() -> Files.size(file))
.thenAnswer(
inv ->
sizeCallCount.incrementAndGet() == 1
? 100L // first read: 100 bytes
: 200L); // second read: 200 bytes — changed!
assertFalse(checker.isReady(file));
}
}
}
// =========================================================================
// Check #6: file-lock check
// =========================================================================
@Nested
@DisplayName("file-lock check")
class FileLockCheck {
@Test
@DisplayName("file held open with exclusive lock by another thread → not ready")
void fileLocked_notReady() throws IOException, InterruptedException {
Path file = realFile("locked.pdf", "data");
setLastModifiedInPast(file, 60_000);
CountDownLatch lockAcquired = new CountDownLatch(1);
CountDownLatch testDone = new CountDownLatch(1);
AtomicInteger lockThreadFailed = new AtomicInteger(0);
Thread lockHolder =
new Thread(
() -> {
try (RandomAccessFile raf =
new RandomAccessFile(file.toFile(), "rw");
FileChannel channel = raf.getChannel();
FileLock lock = channel.lock()) {
lockAcquired.countDown();
testDone.await(10, TimeUnit.SECONDS);
} catch (Exception e) {
lockThreadFailed.set(1);
lockAcquired.countDown();
}
});
lockHolder.setDaemon(true);
lockHolder.start();
lockAcquired.await(5, TimeUnit.SECONDS);
try {
if (lockThreadFailed.get() == 0) {
// Lock was successfully held — the checker must see it as locked.
// On JVM, tryLock() from a second thread in the same process throws
// OverlappingFileLockException (or returns null on some platforms), both of
// which isLocked() maps to true.
assertFalse(checker.isReady(file));
}
// If locking failed on this platform we simply skip the assertion rather than
// failing the build — the logic path is still exercised by other tests.
} finally {
testDone.countDown();
lockHolder.join(5_000);
}
}
@Test
@DisplayName("file with no external lock and all checks passing → ready")
void noLock_ready() throws IOException {
Path file = realFile("unlocked.pdf", "data");
setLastModifiedInPast(file, 60_000);
assertTrue(checker.isReady(file));
}
}
// =========================================================================
// Full happy-path integration
// =========================================================================
@Nested
@DisplayName("full happy path")
class HappyPath {
@Test
@DisplayName("all checks pass → ready")
void allChecksPass_ready() throws IOException {
config.setSettleTimeMillis(5_000);
config.setSizeCheckDelayMillis(1);
config.setAllowedExtensions(List.of("pdf"));
Path file = realFile("invoice.pdf", "PDF content");
setLastModifiedInPast(file, 10_000);
assertTrue(checker.isReady(file));
}
@Test
@DisplayName("first failing check short-circuits evaluation")
void shortCircuitsOnFirstFailure() throws IOException {
// Extension filter will reject — settle / size / lock checks must never run
config.setAllowedExtensions(List.of("pdf"));
config.setSettleTimeMillis(0);
config.setSizeCheckDelayMillis(1);
Path file = realFile("archive.zip", "ZIP data");
setLastModifiedInPast(file, 60_000);
assertFalse(checker.isReady(file));
}
}
// =========================================================================
// Helpers
// =========================================================================
private Path realFile(String name, String content) throws IOException {
Path file = tempDir.resolve(name);
Files.writeString(file, content);
return file;
}
/**
* Back-dates the last-modified time of {@code path} by {@code millisAgo} so that settle-time
* checks pass without actually waiting.
*/
private void setLastModifiedInPast(Path path, long millisAgo) throws IOException {
Files.setLastModifiedTime(
path, FileTime.fromMillis(System.currentTimeMillis() - millisAgo));
}
}
@@ -1,115 +1,76 @@
package stirling.software.common.util;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.io.IOException;
import java.nio.file.Files;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import stirling.software.common.model.api.converters.HTMLToPdfRequest;
import stirling.software.common.service.SsrfProtectionService;
class FileToPdfTest {
public class FileToPdfTest {
private CustomHtmlSanitizer customHtmlSanitizer;
@BeforeEach
void setUp() {
SsrfProtectionService mockSsrfProtectionService = mock(SsrfProtectionService.class);
stirling.software.common.model.ApplicationProperties mockApplicationProperties =
mock(stirling.software.common.model.ApplicationProperties.class);
stirling.software.common.model.ApplicationProperties.System mockSystem =
mock(stirling.software.common.model.ApplicationProperties.System.class);
when(mockSsrfProtectionService.isUrlAllowed(org.mockito.ArgumentMatchers.anyString()))
.thenReturn(true);
when(mockApplicationProperties.getSystem()).thenReturn(mockSystem);
when(mockSystem.isDisableSanitize()).thenReturn(false);
customHtmlSanitizer =
new CustomHtmlSanitizer(mockSsrfProtectionService, mockApplicationProperties);
@Test
void testSanitizeZipFilename_normalFilename() {
String result = FileToPdf.sanitizeZipFilename("document.html");
assertEquals("document.html", result);
}
/**
* Test the HTML to PDF conversion. This test expects an IOException when an empty HTML input is
* provided.
*/
@Test
public void testConvertHtmlToPdf() {
HTMLToPdfRequest request = new HTMLToPdfRequest();
byte[] fileBytes = new byte[0]; // Sample file bytes (empty input)
String fileName = "test.html"; // Sample file name indicating an HTML file
TempFileManager tempFileManager = mock(TempFileManager.class); // Mock TempFileManager
// Mock the temp file creation to return real temp files
try {
when(tempFileManager.createTempFile(anyString()))
.thenReturn(Files.createTempFile("test", ".pdf").toFile())
.thenReturn(Files.createTempFile("test", ".html").toFile());
} catch (IOException e) {
throw new RuntimeException(e);
}
// Expect an IOException to be thrown due to empty input or invalid weasyprint path
Throwable thrown =
assertThrows(
Exception.class,
() ->
FileToPdf.convertHtmlToPdf(
"/path/",
request,
fileBytes,
fileName,
tempFileManager,
customHtmlSanitizer));
assertNotNull(thrown);
void testSanitizeZipFilename_pathTraversal() {
String result = FileToPdf.sanitizeZipFilename("../../etc/passwd");
// Should remove ../ sequences
assertFalse(result.contains(".."), "Path traversal sequences should be removed");
}
/**
* Test sanitizeZipFilename with null or empty input. It should return an empty string in these
* cases.
*/
@Test
public void testSanitizeZipFilename_NullOrEmpty() {
assertEquals("", FileToPdf.sanitizeZipFilename(null));
assertEquals("", FileToPdf.sanitizeZipFilename(" "));
void testSanitizeZipFilename_driveLetterRemoved() {
String result = FileToPdf.sanitizeZipFilename("C:\\Users\\test\\file.html");
assertFalse(result.startsWith("C:"), "Drive letter should be removed");
}
/**
* Test sanitizeZipFilename to ensure it removes path traversal sequences. This includes
* removing both forward and backward slash sequences.
*/
@Test
public void testSanitizeZipFilename_RemovesTraversalSequences() {
String input = "../some/../path/..\\to\\file.txt";
String expected = "some/path/to/file.txt";
// Expect that the method replaces backslashes with forward slashes
// and removes path traversal sequences
assertEquals(expected, FileToPdf.sanitizeZipFilename(input));
void testSanitizeZipFilename_backslashesNormalized() {
String result = FileToPdf.sanitizeZipFilename("path\\to\\file.html");
assertFalse(result.contains("\\"), "Backslashes should be normalized to forward slashes");
assertTrue(result.contains("/") || !result.contains("\\"));
}
/** Test sanitizeZipFilename to ensure that it removes leading drive letters and slashes. */
@Test
public void testSanitizeZipFilename_RemovesLeadingDriveAndSlashes() {
String input = "C:\\folder\\file.txt";
String expected = "folder/file.txt";
assertEquals(expected, FileToPdf.sanitizeZipFilename(input));
input = "/folder/file.txt";
expected = "folder/file.txt";
assertEquals(expected, FileToPdf.sanitizeZipFilename(input));
void testSanitizeZipFilename_nullInput() {
String result = FileToPdf.sanitizeZipFilename(null);
assertEquals("", result, "Null input should return empty string");
}
/** Test sanitizeZipFilename to verify that safe filenames remain unchanged. */
@Test
public void testSanitizeZipFilename_NoChangeForSafeNames() {
String input = "folder/subfolder/file.txt";
assertEquals(input, FileToPdf.sanitizeZipFilename(input));
void testSanitizeZipFilename_emptyInput() {
String result = FileToPdf.sanitizeZipFilename("");
assertEquals("", result, "Empty input should return empty string");
}
@Test
void testSanitizeZipFilename_whitespaceOnly() {
String result = FileToPdf.sanitizeZipFilename(" ");
assertEquals("", result, "Whitespace-only input should return empty string");
}
@Test
void testSanitizeZipFilename_leadingSlashes() {
String result = FileToPdf.sanitizeZipFilename("///path/to/file.html");
assertFalse(result.startsWith("/"), "Leading slashes should be removed");
}
@Test
void testSanitizeZipFilename_nestedDirectories() {
String result = FileToPdf.sanitizeZipFilename("dir1/dir2/file.html");
assertEquals("dir1/dir2/file.html", result, "Normal nested paths should be preserved");
}
@Test
void testSanitizeZipFilename_mixedTraversal() {
String result = FileToPdf.sanitizeZipFilename("dir/../../../etc/passwd");
assertFalse(result.contains(".."), "Mixed path traversal should be removed");
}
@Test
void testSanitizeZipFilename_backslashTraversal() {
String result = FileToPdf.sanitizeZipFilename("dir\\..\\..\\etc\\passwd");
assertFalse(result.contains(".."), "Backslash path traversal should be removed");
}
}
@@ -0,0 +1,163 @@
package stirling.software.common.util;
import static org.junit.jupiter.api.Assertions.*;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
import org.apache.pdfbox.pdmodel.interactive.form.PDCheckBox;
import org.apache.pdfbox.pdmodel.interactive.form.PDComboBox;
import org.apache.pdfbox.pdmodel.interactive.form.PDListBox;
import org.apache.pdfbox.pdmodel.interactive.form.PDPushButton;
import org.apache.pdfbox.pdmodel.interactive.form.PDRadioButton;
import org.apache.pdfbox.pdmodel.interactive.form.PDSignatureField;
import org.apache.pdfbox.pdmodel.interactive.form.PDTerminalField;
import org.apache.pdfbox.pdmodel.interactive.form.PDTextField;
import org.junit.jupiter.api.Test;
class FormFieldTypeSupportTest {
@Test
void forField_withNull_returnsNull() {
assertNull(FormFieldTypeSupport.forField(null));
}
@Test
void forField_withTextField_returnsTEXT() {
try (PDDocument doc = new PDDocument()) {
PDAcroForm form = new PDAcroForm(doc);
PDTextField field = new PDTextField(form);
assertEquals(FormFieldTypeSupport.TEXT, FormFieldTypeSupport.forField(field));
} catch (Exception e) {
fail("Unexpected exception: " + e.getMessage());
}
}
@Test
void forField_withCheckBox_returnsCHECKBOX() {
try (PDDocument doc = new PDDocument()) {
PDAcroForm form = new PDAcroForm(doc);
PDCheckBox field = new PDCheckBox(form);
assertEquals(FormFieldTypeSupport.CHECKBOX, FormFieldTypeSupport.forField(field));
} catch (Exception e) {
fail("Unexpected exception: " + e.getMessage());
}
}
@Test
void forField_withRadioButton_returnsRADIO() {
try (PDDocument doc = new PDDocument()) {
PDAcroForm form = new PDAcroForm(doc);
PDRadioButton field = new PDRadioButton(form);
assertEquals(FormFieldTypeSupport.RADIO, FormFieldTypeSupport.forField(field));
} catch (Exception e) {
fail("Unexpected exception: " + e.getMessage());
}
}
@Test
void forField_withComboBox_returnsCOMBOBOX() {
try (PDDocument doc = new PDDocument()) {
PDAcroForm form = new PDAcroForm(doc);
PDComboBox field = new PDComboBox(form);
assertEquals(FormFieldTypeSupport.COMBOBOX, FormFieldTypeSupport.forField(field));
} catch (Exception e) {
fail("Unexpected exception: " + e.getMessage());
}
}
@Test
void forField_withListBox_returnsLISTBOX() {
try (PDDocument doc = new PDDocument()) {
PDAcroForm form = new PDAcroForm(doc);
PDListBox field = new PDListBox(form);
assertEquals(FormFieldTypeSupport.LISTBOX, FormFieldTypeSupport.forField(field));
} catch (Exception e) {
fail("Unexpected exception: " + e.getMessage());
}
}
@Test
void forField_withSignatureField_returnsSIGNATURE() {
try (PDDocument doc = new PDDocument()) {
PDAcroForm form = new PDAcroForm(doc);
PDSignatureField field = new PDSignatureField(form);
assertEquals(FormFieldTypeSupport.SIGNATURE, FormFieldTypeSupport.forField(field));
} catch (Exception e) {
fail("Unexpected exception: " + e.getMessage());
}
}
@Test
void forField_withPushButton_returnsBUTTON() {
try (PDDocument doc = new PDDocument()) {
PDAcroForm form = new PDAcroForm(doc);
PDPushButton field = new PDPushButton(form);
assertEquals(FormFieldTypeSupport.BUTTON, FormFieldTypeSupport.forField(field));
} catch (Exception e) {
fail("Unexpected exception: " + e.getMessage());
}
}
@Test
void forTypeName_withValidNames_returnsCorrectEnum() {
assertEquals(FormFieldTypeSupport.TEXT, FormFieldTypeSupport.forTypeName("text"));
assertEquals(FormFieldTypeSupport.CHECKBOX, FormFieldTypeSupport.forTypeName("checkbox"));
assertEquals(FormFieldTypeSupport.RADIO, FormFieldTypeSupport.forTypeName("radio"));
assertEquals(FormFieldTypeSupport.COMBOBOX, FormFieldTypeSupport.forTypeName("combobox"));
assertEquals(FormFieldTypeSupport.LISTBOX, FormFieldTypeSupport.forTypeName("listbox"));
assertEquals(FormFieldTypeSupport.SIGNATURE, FormFieldTypeSupport.forTypeName("signature"));
assertEquals(FormFieldTypeSupport.BUTTON, FormFieldTypeSupport.forTypeName("button"));
}
@Test
void forTypeName_withNull_returnsNull() {
assertNull(FormFieldTypeSupport.forTypeName(null));
}
@Test
void forTypeName_withUnknown_returnsNull() {
assertNull(FormFieldTypeSupport.forTypeName("unknown"));
}
@Test
void doesNotSupportsDefinitionCreation_textReturnsFalse() {
assertFalse(FormFieldTypeSupport.TEXT.doesNotsupportsDefinitionCreation());
}
@Test
void doesNotSupportsDefinitionCreation_radioReturnsTrue() {
assertTrue(FormFieldTypeSupport.RADIO.doesNotsupportsDefinitionCreation());
}
@Test
void doesNotSupportsDefinitionCreation_signatureReturnsTrue() {
assertTrue(FormFieldTypeSupport.SIGNATURE.doesNotsupportsDefinitionCreation());
}
@Test
void doesNotSupportsDefinitionCreation_buttonReturnsTrue() {
assertTrue(FormFieldTypeSupport.BUTTON.doesNotsupportsDefinitionCreation());
}
@Test
void createField_text_returnsPDTextField() {
try (PDDocument doc = new PDDocument()) {
PDAcroForm form = new PDAcroForm(doc);
PDTerminalField field = FormFieldTypeSupport.TEXT.createField(form);
assertInstanceOf(PDTextField.class, field);
} catch (Exception e) {
fail("Unexpected exception: " + e.getMessage());
}
}
@Test
void createField_checkbox_returnsPDCheckBox() {
try (PDDocument doc = new PDDocument()) {
PDAcroForm form = new PDAcroForm(doc);
PDTerminalField field = FormFieldTypeSupport.CHECKBOX.createField(form);
assertInstanceOf(PDCheckBox.class, field);
} catch (Exception e) {
fail("Unexpected exception: " + e.getMessage());
}
}
}
@@ -0,0 +1,304 @@
package stirling.software.common.util;
import static org.junit.jupiter.api.Assertions.*;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDResources;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget;
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
import org.apache.pdfbox.pdmodel.interactive.form.PDCheckBox;
import org.apache.pdfbox.pdmodel.interactive.form.PDTextField;
import org.junit.jupiter.api.Test;
class FormUtilsAdditionalTest {
private record SetupDocument(PDPage page, PDAcroForm acroForm) {}
private static SetupDocument createBasicDocument(PDDocument document) throws IOException {
PDPage page = new PDPage();
document.addPage(page);
PDAcroForm acroForm = new PDAcroForm(document);
acroForm.setDefaultResources(new PDResources());
acroForm.setNeedAppearances(true);
document.getDocumentCatalog().setAcroForm(acroForm);
return new SetupDocument(page, acroForm);
}
private static void attachWidget(
SetupDocument setup,
org.apache.pdfbox.pdmodel.interactive.form.PDTerminalField field,
PDRectangle rectangle)
throws IOException {
PDAnnotationWidget widget = new PDAnnotationWidget();
widget.setRectangle(rectangle);
widget.setPage(setup.page);
List<PDAnnotationWidget> widgets = new ArrayList<>(field.getWidgets());
widgets.add(widget);
field.setWidgets(widgets);
setup.acroForm.getFields().add(field);
setup.page.getAnnotations().add(widget);
}
// --- detectFieldType ---
@Test
void testDetectFieldType_textField() throws IOException {
try (PDDocument doc = new PDDocument()) {
SetupDocument setup = createBasicDocument(doc);
PDTextField field = new PDTextField(setup.acroForm);
assertEquals("text", FormUtils.detectFieldType(field));
}
}
@Test
void testDetectFieldType_checkBox() throws IOException {
try (PDDocument doc = new PDDocument()) {
SetupDocument setup = createBasicDocument(doc);
PDCheckBox field = new PDCheckBox(setup.acroForm);
assertEquals("checkbox", FormUtils.detectFieldType(field));
}
}
// --- extractFormFields ---
@Test
void testExtractFormFields_nullDocument() {
List<FormUtils.FormFieldInfo> fields = FormUtils.extractFormFields(null);
assertTrue(fields.isEmpty());
}
@Test
void testExtractFormFields_noAcroForm() throws IOException {
try (PDDocument doc = new PDDocument()) {
doc.addPage(new PDPage());
// No AcroForm set
List<FormUtils.FormFieldInfo> fields = FormUtils.extractFormFields(doc);
assertTrue(fields.isEmpty());
}
}
@Test
void testExtractFormFields_singleTextField() throws IOException {
try (PDDocument doc = new PDDocument()) {
SetupDocument setup = createBasicDocument(doc);
PDTextField textField = new PDTextField(setup.acroForm);
textField.setPartialName("firstName");
attachWidget(setup, textField, new PDRectangle(50, 700, 200, 20));
List<FormUtils.FormFieldInfo> fields = FormUtils.extractFormFields(doc);
assertEquals(1, fields.size());
assertEquals("firstName", fields.get(0).name());
assertEquals("text", fields.get(0).type());
assertEquals(0, fields.get(0).pageIndex());
}
}
@Test
void testExtractFormFields_multipleFields() throws IOException {
try (PDDocument doc = new PDDocument()) {
SetupDocument setup = createBasicDocument(doc);
PDTextField field1 = new PDTextField(setup.acroForm);
field1.setPartialName("name");
attachWidget(setup, field1, new PDRectangle(50, 700, 200, 20));
PDTextField field2 = new PDTextField(setup.acroForm);
field2.setPartialName("email");
attachWidget(setup, field2, new PDRectangle(50, 660, 200, 20));
List<FormUtils.FormFieldInfo> fields = FormUtils.extractFormFields(doc);
assertEquals(2, fields.size());
}
}
// --- buildFillTemplateRecord ---
@Test
void testBuildFillTemplateRecord_null() {
Map<String, Object> result = FormUtils.buildFillTemplateRecord(null);
assertTrue(result.isEmpty());
}
@Test
void testBuildFillTemplateRecord_empty() {
Map<String, Object> result = FormUtils.buildFillTemplateRecord(Collections.emptyList());
assertTrue(result.isEmpty());
}
@Test
void testBuildFillTemplateRecord_textField() {
FormUtils.FormFieldInfo info =
new FormUtils.FormFieldInfo(
"name", "Name", "text", "John", null, false, 0, false, null, 0);
Map<String, Object> result = FormUtils.buildFillTemplateRecord(List.of(info));
assertEquals("John", result.get("name"));
}
@Test
void testBuildFillTemplateRecord_checkboxField() {
FormUtils.FormFieldInfo info =
new FormUtils.FormFieldInfo(
"agree", "Agreement", "checkbox", "Yes", null, false, 0, false, null, 0);
Map<String, Object> result = FormUtils.buildFillTemplateRecord(List.of(info));
assertEquals(Boolean.TRUE, result.get("agree"));
}
@Test
void testBuildFillTemplateRecord_checkboxFieldOff() {
FormUtils.FormFieldInfo info =
new FormUtils.FormFieldInfo(
"agree", "Agreement", "checkbox", "Off", null, false, 0, false, null, 0);
Map<String, Object> result = FormUtils.buildFillTemplateRecord(List.of(info));
assertEquals(Boolean.FALSE, result.get("agree"));
}
@Test
void testBuildFillTemplateRecord_skipsButton() {
FormUtils.FormFieldInfo info =
new FormUtils.FormFieldInfo(
"submit", "Submit", "button", null, null, false, 0, false, null, 0);
Map<String, Object> result = FormUtils.buildFillTemplateRecord(List.of(info));
assertFalse(result.containsKey("submit"));
}
@Test
void testBuildFillTemplateRecord_skipsSignature() {
FormUtils.FormFieldInfo info =
new FormUtils.FormFieldInfo(
"sig", "Signature", "signature", null, null, false, 0, false, null, 0);
Map<String, Object> result = FormUtils.buildFillTemplateRecord(List.of(info));
assertFalse(result.containsKey("sig"));
}
// --- safeValue ---
@Test
void testSafeValue_nonNull() {
assertEquals("hello", FormUtils.safeValue("hello"));
}
@Test
void testSafeValue_null() {
assertEquals("", FormUtils.safeValue(null));
}
// --- applyFieldValues ---
@Test
void testApplyFieldValues_nullDocument() throws IOException {
// Should not throw
FormUtils.applyFieldValues(null, Map.of("key", "value"), false);
}
@Test
void testApplyFieldValues_noAcroFormStrict() throws IOException {
try (PDDocument doc = new PDDocument()) {
doc.addPage(new PDPage());
assertThrows(
IOException.class,
() -> FormUtils.applyFieldValues(doc, Map.of("key", "val"), false, true));
}
}
@Test
void testApplyFieldValues_noAcroFormNonStrict() throws IOException {
try (PDDocument doc = new PDDocument()) {
doc.addPage(new PDPage());
// Should not throw in non-strict mode
FormUtils.applyFieldValues(doc, Map.of("key", "val"), false, false);
}
}
@Test
void testApplyFieldValues_setsTextValue() throws IOException {
try (PDDocument doc = new PDDocument()) {
SetupDocument setup = createBasicDocument(doc);
PDTextField textField = new PDTextField(setup.acroForm);
textField.setPartialName("company");
attachWidget(setup, textField, new PDRectangle(60, 720, 220, 20));
FormUtils.applyFieldValues(doc, Map.of("company", "Stirling"), false);
assertEquals("Stirling", textField.getValueAsString());
}
}
@Test
void testApplyFieldValues_checksCheckbox_nonStrict() throws IOException {
// In non-strict mode, checkbox state changes may fail silently
// if appearance streams are not properly configured. Just verify no exception.
try (PDDocument doc = new PDDocument()) {
SetupDocument setup = createBasicDocument(doc);
PDCheckBox checkBox = new PDCheckBox(setup.acroForm);
checkBox.setPartialName("subscribed");
checkBox.setExportValues(List.of("Yes"));
attachWidget(setup, checkBox, new PDRectangle(60, 680, 16, 16));
// Should not throw in non-strict mode even if appearance is missing
FormUtils.applyFieldValues(doc, Map.of("subscribed", true), false, false);
FormUtils.applyFieldValues(doc, Map.of("subscribed", false), false, false);
}
}
// --- filterSingleChoiceSelection ---
@Test
void testFilterSingleChoiceSelection_validSelection() {
String result =
FormUtils.filterSingleChoiceSelection(
"Option A", List.of("Option A", "Option B"), "field1");
assertEquals("Option A", result);
}
@Test
void testFilterSingleChoiceSelection_invalidSelection() {
String result =
FormUtils.filterSingleChoiceSelection(
"Invalid", List.of("Option A", "Option B"), "field1");
assertNull(result);
}
@Test
void testFilterSingleChoiceSelection_nullSelection() {
String result = FormUtils.filterSingleChoiceSelection(null, List.of("Option A"), "field1");
assertNull(result);
}
@Test
void testFilterSingleChoiceSelection_emptySelection() {
String result = FormUtils.filterSingleChoiceSelection(" ", List.of("Option A"), "field1");
assertNull(result);
}
// --- extractFieldsWithTemplate ---
@Test
void testExtractFieldsWithTemplate_emptyDocument() throws IOException {
try (PDDocument doc = new PDDocument()) {
doc.addPage(new PDPage());
FormUtils.FormFieldExtraction extraction = FormUtils.extractFieldsWithTemplate(doc);
assertNotNull(extraction);
assertTrue(extraction.fields().isEmpty());
assertTrue(extraction.template().isEmpty());
}
}
// --- hasAnyRotatedPage ---
@Test
void testHasAnyRotatedPage_noRotation() throws IOException {
try (PDDocument doc = new PDDocument()) {
doc.addPage(new PDPage());
assertFalse(FormUtils.hasAnyRotatedPage(doc));
}
}
}
@@ -1,4 +1,4 @@
package stirling.software.proprietary.util;
package stirling.software.common.util;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -0,0 +1,97 @@
package stirling.software.common.util;
import static org.junit.jupiter.api.Assertions.*;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.junit.jupiter.api.Test;
class GeneralFormCopyUtilsTest {
@Test
void hasAnyRotatedPage_noRotation_returnsFalse() throws Exception {
try (PDDocument doc = new PDDocument()) {
doc.addPage(new PDPage());
doc.addPage(new PDPage());
assertFalse(GeneralFormCopyUtils.hasAnyRotatedPage(doc));
}
}
@Test
void hasAnyRotatedPage_with90Rotation_returnsTrue() throws Exception {
try (PDDocument doc = new PDDocument()) {
PDPage page = new PDPage();
page.setRotation(90);
doc.addPage(page);
assertTrue(GeneralFormCopyUtils.hasAnyRotatedPage(doc));
}
}
@Test
void hasAnyRotatedPage_with180Rotation_returnsTrue() throws Exception {
try (PDDocument doc = new PDDocument()) {
PDPage page = new PDPage();
page.setRotation(180);
doc.addPage(page);
assertTrue(GeneralFormCopyUtils.hasAnyRotatedPage(doc));
}
}
@Test
void hasAnyRotatedPage_with360Rotation_returnsFalse() throws Exception {
try (PDDocument doc = new PDDocument()) {
PDPage page = new PDPage();
page.setRotation(360);
doc.addPage(page);
assertFalse(GeneralFormCopyUtils.hasAnyRotatedPage(doc));
}
}
@Test
void hasAnyRotatedPage_emptyDocument_returnsFalse() throws Exception {
try (PDDocument doc = new PDDocument()) {
assertFalse(GeneralFormCopyUtils.hasAnyRotatedPage(doc));
}
}
@Test
void hasAnyRotatedPage_mixedPages_returnsTrue() throws Exception {
try (PDDocument doc = new PDDocument()) {
doc.addPage(new PDPage());
PDPage rotated = new PDPage();
rotated.setRotation(270);
doc.addPage(rotated);
assertTrue(GeneralFormCopyUtils.hasAnyRotatedPage(doc));
}
}
@Test
void copyAndTransformFormFields_noAcroForm_doesNotThrow() throws Exception {
try (PDDocument source = new PDDocument();
PDDocument target = new PDDocument()) {
source.addPage(new PDPage());
target.addPage(new PDPage());
// No acro form set on source - should simply return without error
assertDoesNotThrow(
() ->
GeneralFormCopyUtils.copyAndTransformFormFields(
source, target, 1, 1, 1, 1, 612f, 792f));
}
}
@Test
void copyAndTransformFormFields_emptyAcroForm_doesNotThrow() throws Exception {
try (PDDocument source = new PDDocument();
PDDocument target = new PDDocument()) {
source.addPage(new PDPage());
target.addPage(new PDPage());
// Empty acro form
var acroForm = new org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm(source);
source.getDocumentCatalog().setAcroForm(acroForm);
assertDoesNotThrow(
() ->
GeneralFormCopyUtils.copyAndTransformFormFields(
source, target, 1, 1, 1, 1, 612f, 792f));
}
}
}

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