Compare commits

...

192 Commits

Author SHA1 Message Date
Anthony Stirling
d59cb18666 init docker 2024-12-09 18:18:16 +00:00
Anthony Stirling
a772b4fa09 english 2024-12-06 12:03:41 +00:00
github-actions[bot]
b072c39fd9 📝 Update README: Translation Progress Table (#2406)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-12-06 10:28:05 +00:00
Ludy
1bc6b4149c Update German translation + remove unnecessary comment (#2405) 2024-12-06 10:23:00 +00:00
Anthony Stirling
5a5a8bb7ba quick fixes (#2404) 2024-12-05 19:55:34 +00:00
github-actions[bot]
b6eca59f23 📝 Update README: Translation Progress Table (#2402)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-12-05 19:54:34 +00:00
Anthony Stirling
7108424a92 Update build.gradle 2024-12-05 19:16:18 +00:00
albanobattistella
400965ffc8 Update messages_it_IT.properties (#2401) 2024-12-05 17:39:49 +00:00
github-actions[bot]
1895a04394 📝 Update README: Translation Progress Table (#2399)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-12-05 17:00:41 +00:00
Omar Ahmed Hassan
f8f137a30a Feature: Show permissions as a separate tab (#2396)
Show permissions as a separate tab

- Move permissions code into a separate for better readability and maintainability.
- Separate `Permissions` node from `Encryption` so that it would be displayed in the frontend as a separate tab.
- Use more user friendly permission labels such as replacing `canModify` with `Modifying` and values such as `Allowed` and `Not Allowed` instead of `true`, `false`.
- Show permissions regardless of the encryption state.
2024-12-05 17:00:23 +00:00
github-actions[bot]
f6a2d4784b Update translation files (#2398)
Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-12-05 16:02:35 +00:00
reecebrowne
526dc9f911 Only download one file on sign cert (#2397) 2024-12-05 15:58:27 +00:00
Anthony Stirling
cce9f74eb9 PDF Cert validation (#2394)
* verifyCerts

* cert info

* Hardening suggestions for Stirling-PDF / certValidate (#2395)

* Protect `readLine()` against DoS

* Switch order of literals to prevent NullPointerException

---------

Co-authored-by: pixeebot[bot] <104101892+pixeebot[bot]@users.noreply.github.com>

* some basic html excaping and translation fixing

---------

Co-authored-by: pixeebot[bot] <104101892+pixeebot[bot]@users.noreply.github.com>
Co-authored-by: a <a>
2024-12-05 15:56:22 +00:00
Omar Ahmed Hassan
0e3865618d Fix missing upload button (#2393)
The code snippet `input[type=file]{ display: none;}` was unintentionally hiding the upload button, to fix this, it was changed to only target input within `.input-container`
2024-12-05 11:16:16 +00:00
reecebrowne
d888ed1ae0 Feature/undo page break (#2389)
* Fix delete selected
Fix add page break where selected
Added undo logic for page breaks

* Add pages undo capability

* Fix page break when selected logic
2024-12-05 10:43:31 +00:00
Anthony Stirling
99d1b46d97 Update MetricsAggregatorService.java 2024-12-03 15:26:40 +00:00
Anthony Stirling
32e46eeb73 Update build.gradle 2024-12-03 10:54:07 +00:00
Omar Ahmed Hassan
b7da84d257 Fix deserialization failure in Change Metadata (#2382)
* Fix deserialization failure from String to Map

Fix deserialization failure from String to Map that caused the following exception:
Resolved [org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public org.springframework.http.ResponseEntity<byte[]> stirling.software.SPDF.controller.api.misc.MetadataController.metadata(stirling.software.SPDF.model.api.misc.MetadataRequest) throws java.io.IOException: [Field error in object 'metadataRequest' on field 'allRequestParams': rejected value [{"customKey1" : "YourCustomKey", "customKeyValue1", "YourCustomValue"}]; codes [typeMismatch.metadataRequest.allRequestParams,typeMismatch.allRequestParams,typeMismatch.java.util.Map,typeMismatch];

* Fix form binding for dynamic Map entries in Change Metadata

- Implemented support for dynamic key-value inputs in Change Metadata form using proper `name` attributes for Map (`allRequestParams`) binding.
- Fix form binding for dynamic Map (`allRequestParams`) entries in Change Metadata as the `allRequestParams` (Map name) was being sent as an empty map.
2024-12-03 08:28:34 +00:00
github-actions[bot]
1c1ead5d62 📝 Update README: Translation Progress Table (#2381)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-12-02 19:25:16 +00:00
albanobattistella
6ff53aa5b3 Update messages_it_IT.properties (#2380) 2024-12-02 18:59:05 +00:00
github-actions[bot]
8d60b08cd9 📝 Update README: Translation Progress Table (#2379)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-12-02 18:32:07 +00:00
github-actions[bot]
64cf5167c0 Update translation files (#2378)
Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-12-02 18:18:16 +00:00
Omar Ahmed Hassan
de4637e8d4 Fix drag and drop area for file choosers by adding separate ones (#2368)
* Add separate drag and drop area for file choosers

 - Add separate drag and drop area for file choosers

### Why?
Previously, when there were multiple file choosers in the same page, if you attempted to drag and drop any files, they would be added to both file choosers as it was designed at first to handle 1 file chooser present, now that we have multiple ones, it is necessary to adapt our design to match the changing functionality.

### Can you not preserve the old overlay when there's only one file chooser present?
Yes, we can, but imagine as a user, you try to drag and drop a file in one page and the fields turn into drag and drop areas then you go to another page and try to drag and drop again but you encounter the old overlay instead, as a user you might get confused and ask yourself "What changed?" or if a user is telling another user the steps to drag and drop files and he didn't know about this case, then it would still be confusing, thus consistency is preferred in this case.

* Update file chooser UI

* Add support for listing and removing selected files and their file icons

- Selected files are listed below the file chooser in a selected files container.
- Users can now remove uploaded/selected files.
- Hide selected files container/box unless there are files selected/uploaded.
- Add separate overlay for each drag & drop area.

## FAQ:
- Why did you assign a unique id to each file? isn't the filename enough?
= Because a user might upload multiple files with the same name, if the user wanted to remove one of them, how would we differentiate between them? we won't be able to unless we assign an identifier, you might argue "we remove based on the filename and size", then what if the user uploaded the same file more than once (intentionally), then we would accidentally remove all the files even though that is not what the user wanted, so going with unique ID approach would prevent this issue/problem from occurring in the first place.

* Rename remove-file css class to remove-selected-file

- Rename remove-file css class to remove-selected-file to avoid css conflict with remove-file in merge.css

* Use input element to dispatch event on file removal

Use the correct element to dispatch "file-input-change" (input element is the correct one).

* Adapt file chooser UI to themes

- Adapt file chooser UI to themes by adjusting their font colors and background colors.
- Make text more visible in overlay by increasing the font size by 0.1rem and setting font weight to 550.

* Remove extra overlay border

- Removing overlay's border as it is unnecessary and only causing a double border issue on the file input container.

* Remove Browse button, highlight file chooser and make it clickable

- Remove browse button.
- Make the entire file chooser container clickable.
- Add glowing effect on hover for file chooser.
- Change color of file chooser on hover.

* Replace crypto.randomUUID() with UUID.uuidv4()

- Replace crypto.randomUUID() with UUID.uuidv4() as crypto.randomUUID() is only supported in secured contexts such as localhost 127.0.0.1 and over HTTPS

* Fix merge file removal not being reflected in file chooser

- Files removed from the list in merge page would now be reflected in the file chooser's container.

* Make inputElement optional in removeFileById

- Make inputElement optional in removeFileById, this way we could control changing inputElements files.

* Add translation support to file chooser

---------

Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2024-12-02 18:10:12 +00:00
Sai Kumar
3c0a8071dc added support for new line break in stampController (#2370)
added support for new line in stampController

Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2024-12-02 17:48:19 +00:00
Omar Ahmed Hassan
04ccdf6f76 Fix: prevent fileInput.js from adding event listeners more than once (#2365)
Fix fileInput.js adding event listeners more than once

- Fix a bug that caused fileInput.js to add event listeners more than once per HTML file as it's included in fileSelector fragment in fragments/common.html thus it's being loaded N times where N is the number of file selectors / custom file chooser / file input elements per HTML file, which resulted in each event actions being executed N times as well, which was prevalent in drag and drop operations such as dragging and dropping a file called y.png, it would be duplicated N times (as in /sign path).
2024-12-02 17:41:11 +00:00
Omar Ahmed Hassan
db02fba31f Fix translations for watermark spacers (#2369)
Fix translations by adding a space between width/height and spacer and capitalize the first letter
2024-12-02 17:01:19 +00:00
Omar Ahmed Hassan
5b6f649e4e Fix submit button in crop by adding id (#2374)
- Add missing ID to submit button in crop page.
2024-12-02 10:40:46 +00:00
Omar Ahmed Hassan
de23bb702c Fix allowing multiple files to be dropped onto a single file input (#2359)
Fix a bug that allowed multiple files to be dropped onto a single-file input element

- Fix a bug that allowed multiple files to be dropped onto a single-file input element by accepting only the first file.
2024-11-29 17:31:14 +00:00
github-actions[bot]
25e564154e Update 3rd Party Licenses (#2362)
Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: GitHub Action <action@github.com>
2024-11-29 15:41:29 +00:00
Anthony Stirling
3633a979d3 fixes and other changes and debug of WIP SAML (#2360)
* backup

* remove debugs

* oauth to saml and compare fixes etc

* ee flag for saml

* more fixes

* info to debug

* remove unused repo

* spring dev fix for saml

* debugs

* saml stuff

* debugs

* fix
2024-11-29 15:11:59 +00:00
Omar Ahmed Hassan
99d481d69f Fix Array.from syntax in nonmultiple file upload (#2357)
- Fix Array.from syntax in nonmultiple file upload as Array.from(<non-array or string>) returns an empty array which is the case when a file is selected from an input element (when multiple attribute isn't  supported) which can be found in Array.from(element.files[0]) -> results in an empty array.
2024-11-29 12:22:52 +00:00
github-actions[bot]
a5ba6c403a 📝 Update README: Translation Progress Table (#2356)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-11-28 15:39:39 +00:00
albanobattistella
b2e6d89d16 Update messages_it_IT.properties (#2355) 2024-11-28 14:42:55 +00:00
github-actions[bot]
b59d2d15b4 Update translation files (#2354)
Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-11-28 14:42:29 +00:00
Omar Ahmed Hassan
61e750646c Feature: Undo Redo options multi tool #2297 (#2348)
* Implement Command class for Command Pattern

Created a base `Command` class to implement the **Command Pattern**. This class provides a skeletal implementation for `execute`, `undo`, and `redo` methods.

**Note:** This class is intended to be subclassed and not instantiated directly.

* Add undo/redo stacks and operations

* Use rotate element command to perform execute/undo/redo operations

* Handle commands executed through events
- Add "command-execution" event listener to execute commands that are not invoked from the same class while adding the command to the undo stack and clearing the redo stack.

* Add and use rotate all command to rotate/redo/undo all elements

* Use command pattern to delete pages

* Use command pattern for page selection

* Use command pattern to move pages up and down

* Use command pattern to remove selected pages

* Use command pattern to perform the splitting operation

* Add undo/redo functionality with filename input exclusion

- Implement undo (Ctrl+Z) and redo (Ctrl+Y) functionality.
- Prevent undo/redo actions when the filename input field is focused.
- Ensures proper handling of undo/redo actions without interfering with text editing.

* Introduce UndoManager for managing undo/redo operations

 - Encapsulate undo/redo stacks and operations within UndoManager.
- Simplify handling of undo/redo functionality through a dedicated manager.

* Call execute on splitAllCommand

- Fix a bug that caused split all functionality to not work as execute() wasn't called on splitAllCommand

* Add undo/redo buttons to multi tool

- Add undo/redo buttons to multi tool
- Dispatch an event upon state change (such as changes in the undo/redo stacks) to update the UI accordingly.

* Add undo/redo to translations

* Replace hard-coded "Undo"/"Redo" with translation keys in multi tool

---------

Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2024-11-28 14:25:13 +00:00
Omar Ahmed Hassan
de9c21b3de Fix: page break insertion functionality in Multi Tool (#2350)
Fix page break insertion functionality

- Page Break insertion functionality now successfully inserts page breaks upon request
2024-11-28 10:21:14 +00:00
Nureddin Farzaliyev
b32d6cb858 Azerbaijani Language Translation (#2347)
* Azerbaijani flag and dropdown item added

* Azerbaijani Language file Added

* AZ - ignore_translation.toml init

* AZ Translation Enterprise Edition Section

* Translation for Generic

* translation-az pipeline

* Translation for Analytics

* Translation for NAVBAR

* Translation for SETTINGS

* translation-az homepage

* Translation for #login

* Translation for (showJS)

* Translation for #showJS

* Translation for #PDFToWord

* Translation for #PDFToPresentation

* Translation for #PDFToText

* Translation for #PDFToHTML

* Translation for #PDFToXML

* Translation for #PDFToCSV

* Translation for #repair

* Translation for #pageLayout

* Translation for #pdfToSinglePage

* Translation for #pageExtracter

* Translation for #getPdfInfo

* Translation for #markdown-to-pdf

* Translation for #PDFToXML

* Translation for #html-to-pdf

* Translation for #PDFToHTML

* Translation for #PDFToText

* Translation for #PDFToPresentation

* Translation for #PDFToWord

* Translation for #PDFToCSV

* Translation for #url-to-pdf

* Translation for #pdfToImage

* Translation for #BookToPDF

* Translation for #PDFToBook

* Translation for #autoRedact

* Translation for #Add image

* Translation for #File to PDF

* Translation for (remove-image)

* Translation for (remove-image)

* Translation for (survey)

* Translation for (licenses)

* Translation for (printFile)

* Translation for (split-bysections)

* Translation for (overlay-pdfs)

* Translation for (split-by-size-or-count)

* Translation for (addPageNumbers)

* Translation for (adjustContrast)

* Translation for (autoSplitPDF)

* Translation for (scalePages)

* Translation for (removeCertSign)

* Translation for (removeAnnotations)

* Translation for (sign)

* Translation for (flatten)

* Translation for (extractImages)

* Translation for (merge)

* Translation for (view pdf)

* Translation for (pageRemover)

* Translation for (rotate)

* az Translation for replace-invert-color

* az Translation for addstamprequest

* Translation for (remove-image)

* Translation for #url-to-pdf

* Update messages_az_AZ.properties

* Update README.md

* Update README.md

* Update messages_az_AZ.properties

* Translation for #compress

* Translation for #Change permissions

* Translation for #remove password

* Translation for #pdfToPDFA

* Translation for #changeMetadata

* Translation for #merge

* Translation for #split-pdfs

* translation-az addpass

* translation-az watermark

* translation-az removeblanks

* translation-az compare

* translation-az certsign

* Translation for #pdfOrganiser

* Translation for #multiTool

* Translation for #multiTool

* az translation scannerimagesplit

* az translation ocr

* az translation fix

* az translation linebreak fix

* az translation linebreak fix

---------

Co-authored-by: Lucifer25x <lucifer25x@protonmail.com>
Co-authored-by: islamd7 <vusal04999@gmail.com>
Co-authored-by: Valida Rahmanova <validerehmanova04@gmail.com>
Co-authored-by: yusif043-bit <yusif.abbaszade.043@gmail.com>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2024-11-27 18:09:42 +00:00
pixeebot[bot]
d832a90de0 (CodeQL) Fixed finding: "Arbitrary file access during archive extraction ("Zip Slip")
" (#2344)

(CodeQL) Fixed finding: "Arbitrary file access during archive extraction ("Zip Slip")
"

Co-authored-by: pixeebot[bot] <104101892+pixeebot[bot]@users.noreply.github.com>
2024-11-27 07:16:03 +00:00
Anthony Stirling
212e521238 Update MetricsAggregatorService.java 2024-11-26 21:30:47 +00:00
github-actions[bot]
0915e72a3d 📝 Update README: Translation Progress Table (#2341)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-11-26 20:52:54 +00:00
github-actions[bot]
ee5013651f Update 3rd Party Licenses (#2342)
Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: GitHub Action <action@github.com>
2024-11-26 20:52:39 +00:00
github-actions[bot]
4aa44e6fc0 Update translation files (#2343)
Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-11-26 20:51:57 +00:00
github-actions[bot]
41c743a9f8 Update 3rd Party Licenses (#2337)
Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: GitHub Action <action@github.com>
2024-11-26 20:51:02 +00:00
Anthony Stirling
833b3c45c6 Removal of Ghostscript to use qpdf and tesseract directly (#2338)
* navbar fix multi tool and compress location

* release notes and ghostscript removal

* cleanups

* formatting

* update docs

* more

* more

* docs

* release bump

* Hardening suggestions for Stirling-PDF / ghostscript (#2339)

* Protect `readLine()` against DoS

* Sanitized user-provided file names in HTTP multipart uploads

---------

Co-authored-by: pixeebot[bot] <104101892+pixeebot[bot]@users.noreply.github.com>

---------

Co-authored-by: pixeebot[bot] <104101892+pixeebot[bot]@users.noreply.github.com>
2024-11-26 20:50:35 +00:00
Omar Ahmed Hassan
654bc94d44 Fix: input file overwrite in merge (#2335)
* Fix input files being overwritten by newly uploaded files

- Fix a bug that caused existing selected/uploaded files to be overwritten when a new input file is uploaded through input element.
- Add source property to change event to differentiate between uploaded files using input element and drag/drop uploads to avoid processing drag/drop files more than once, thus avoiding file duplication (file duplication resulting from copying drop/drop files to input files on each 'change' event).

* Dispatch and use file-input-change instead of change event for merging

- Dispatch "file-input-change" event after each "change" event in file upload, to notify other functions/components relying on the files provided by the \<input\> element.
- Use "file-input-change" instead of "change" event to display the latest version of uploaded files.

# FAQ:
- Why use "file-input-change" instead of "change" in merge.js?
= "change" event is automatically triggered when a file is uploaded through \<input\> element which would replace all the existing selected/uploaded files including the drag/drop files.

## Example:
Let's say that the user wants to upload/select the x.pdf, y.pdf and z.pdf all together:

- user selects "x.pdf" -> file selected successfully.
= selected files: x.pdf

- user drags and drops "y.pdf" -> file dropped successfully
= selected files: x.pdf, y.pdf

- user selects again using \<input\> "z.pdf" -> file selected succesfully overwriting selected files.
= selected files: z.pdf
2024-11-26 20:41:08 +00:00
dependabot[bot]
86fa404c90 Bump commons-io:commons-io from 2.17.0 to 2.18.0 (#2333)
Bumps commons-io:commons-io from 2.17.0 to 2.18.0.

---
updated-dependencies:
- dependency-name: commons-io:commons-io
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-26 20:38:51 +00:00
Anthony Stirling
1db1370420 Update README.md 2024-11-26 10:26:44 +00:00
github-actions[bot]
ee4b7e02ab 📝 Update README: Translation Progress Table (#2327)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-11-26 08:17:56 +00:00
albanobattistella
e6c5634165 Update messages_it_IT.properties (#2334) 2024-11-26 08:17:09 +00:00
Anthony Stirling
5188eb3b04 Update build.gradle 2024-11-26 08:16:45 +00:00
Anthony Stirling
3fa6bcb2ee navbar fix multi tool and compress location (#2331) 2024-11-25 21:42:49 +00:00
github-actions[bot]
0b359ad4a8 Update translation files (#2329)
Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-11-25 20:55:56 +00:00
reecebrowne
23ee77f6ab Additional sign tooltips (#2328)
* Add tooltip to sign add to all pages feature

* Additional Tooltips
2024-11-25 20:43:05 +00:00
github-actions[bot]
bfc1ed2b39 Update translation files (#2326)
Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-11-25 20:13:03 +00:00
reecebrowne
da46d942ba Add tooltip to sign add to all pages feature (#2325) 2024-11-25 20:11:27 +00:00
Anthony Stirling
5936e856f0 metrics 2024-11-25 14:02:17 +00:00
github-actions[bot]
fd906d36dd Update 3rd Party Licenses (#2321)
Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: GitHub Action <action@github.com>
2024-11-24 17:21:54 +00:00
Ludy
8f4709d82e Bump com.h2database:h2 from 2.1.214 to 2.3.232 (#2314) 2024-11-24 14:36:53 +00:00
dependabot[bot]
8445f2719b Bump org.springframework:spring-webmvc from 6.1.14 to 6.2.0 (#2268)
Bumps [org.springframework:spring-webmvc](https://github.com/spring-projects/spring-framework) from 6.1.14 to 6.2.0.
- [Release notes](https://github.com/spring-projects/spring-framework/releases)
- [Commits](https://github.com/spring-projects/spring-framework/compare/v6.1.14...v6.2.0)

---
updated-dependencies:
- dependency-name: org.springframework:spring-webmvc
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2024-11-24 14:36:30 +00:00
github-actions[bot]
eaa64e1471 Update 3rd Party Licenses (#2318)
Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: GitHub Action <action@github.com>
2024-11-24 12:16:01 +00:00
Ludy
4abb0cb85e Fix: id for submit button added (#2320) 2024-11-24 10:31:50 +00:00
Omar Ahmed Hassan
afad06bed4 Extract tables from PDF to CSV using Tabula (#2312)
* Add Tabula dependency and exclude slf4j-simple

- Add tabula-java dependency to extract tables into CSV.
- Exclude slf4j-simple due to Logback

* Add a flexible CSVWriter

- Add FlexibleCSVWriter which extends CSVWriter to pass a custom CSVFormat, as CSVWriter's parameterized constructor (that allows changing CSVFormat) is protected.

* Use Tabula in extracting tables from PDF

- Use Tabula in extracting tables from PDF instead of the existing implementation

* Delete PDFTableStripper as It is unneeded

- Delete PDFTableStripper as It is unneeded as Tabula-Java is used instead.

* Use correct class in ExtractCSVController logger

* Exclude gson and bcprov-jdk15on dependencies from tabula

- Exclude gson and bcprov-jdk15on from tabula-java due to detected security vulnerabilities.
2024-11-23 23:28:44 +00:00
github-actions[bot]
faa8a9752c 📝 Update README: Translation Progress Table (#2317)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-11-23 23:05:40 +00:00
Omar Ahmed Hassan
989538e340 Update Arabic Language for Multi tool section (#2316)
* Update Arabic Language for Multi tool section

* Add a back line at 955-956
2024-11-23 22:36:21 +00:00
Ludy
5b8bdc3352 improves readability (#2313) 2024-11-23 22:09:46 +00:00
Thomas BERNARD
f559eaa4e8 French translation (again) (#2315)
* French translations for multiTool

* fix french translation invalidPasswordMessage/confirmPasswordErrorMessage

* french translation : reset/navbar.search/sign.saved

* fix my invalidPasswordMessage french translation :)
2024-11-23 22:09:27 +00:00
github-actions[bot]
f306e00fba Update 3rd Party Licenses (#2310)
Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: GitHub Action <action@github.com>
2024-11-23 12:16:20 +00:00
github-actions[bot]
e09d6f9998 📝 Update README: Translation Progress Table (#2311)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-11-23 12:16:16 +00:00
Ludy
3a27aa16d5 Improves security when processing properties files (#2303)
* Improves security when processing properties files

* Check for spaces in the key

---------

Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2024-11-23 11:49:49 +00:00
Ludy
9a96109ea2 Fix: Prevents duplicate listing of search results (#2306) 2024-11-23 11:37:13 +00:00
albanobattistella
ad1cce378f Update messages_it_IT.properties (#2307)
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2024-11-23 10:53:26 +00:00
Ludy
9abb105835 Fix: Fixes dependency bug and replaces obsolete method (#2309) 2024-11-23 10:51:17 +00:00
Renan
204bae3bc1 Sign multiple PDF pages at the same time in the same location (#2008) (#2278)
* Sign multiple PDF pages at the same time in the same location (#2008)

* Modifying the functionality of how the signature is added to all pages (#2008)

* Adding the functionality to reverse the addition on all pages and implementing buttons to navigate to the first and last pages (#2008)
2024-11-22 17:40:09 +00:00
reecebrowne
547f23fe78 Posthog multitool (#2301)
* Posthog functionality

* Posthog in multitool

* check if anylitics enabled
2024-11-22 17:38:44 +00:00
Rafael Encinas
543ad083a2 Fix file clear for errors (#2302)
* Prevent file input from being removed when an error occurs

* Fix a bug preventing fetch when 'Bored waiting' btn isn't present
2024-11-22 17:11:23 +00:00
github-actions[bot]
61bccd1d8b 📝 Update README: Translation Progress Table (#2300)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-11-22 15:27:11 +00:00
github-actions[bot]
83be709299 Update translation files (#2298)
Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-11-22 15:17:04 +00:00
reecebrowne
0e602153f3 Feature/2198/multitool multi select move pages (#2294)
* Multitool - Select multiple pages for rotation tool

* Multitool multi select delete feature

* Multitool multi select UI improvements and big fixes

* Multitool multi select select all and UI improvements

* Multi tool multi select, download selected, clean up and bug fixes

* Groundwork for multiselect drag and drop

* Multi select drag and drop finalised

* Update translation files

Signed-off-by: GitHub Action <action@github.com>

* Turn off select mode after multidrag

---------

Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-11-22 11:39:22 +00:00
Anthony Stirling
597619740a Update build.gradle 2024-11-22 09:29:41 +00:00
github-actions[bot]
41a39a0a94 Update 3rd Party Licenses (#2295)
Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: GitHub Action <action@github.com>
2024-11-22 09:22:03 +00:00
dependabot[bot]
b14fba064d Bump org.springframework.security:spring-security-saml2-service-provider from 6.3.4 to 6.4.1 (#2296)
Bump org.springframework.security:spring-security-saml2-service-provider

Bumps [org.springframework.security:spring-security-saml2-service-provider](https://github.com/spring-projects/spring-security) from 6.3.4 to 6.4.1.
- [Release notes](https://github.com/spring-projects/spring-security/releases)
- [Changelog](https://github.com/spring-projects/spring-security/blob/main/RELEASE.adoc)
- [Commits](https://github.com/spring-projects/spring-security/compare/6.3.4...6.4.1)

---
updated-dependencies:
- dependency-name: org.springframework.security:spring-security-saml2-service-provider
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-22 09:19:58 +00:00
alonsofabila-dev
bd29dd1ac3 Bored waiting button doesnt remove itself after processing (#2079) (#2235)
Fix: Bored waiting button doesnt remove itself after processing (#2079)

hide bored waiting? button after request handling both success and error cases to properly hide the button.

Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2024-11-22 09:14:26 +00:00
dependabot[bot]
6d3f14375e Bump bouncycastleVersion from 1.78.1 to 1.79 (#2177)
Bumps `bouncycastleVersion` from 1.78.1 to 1.79.

Updates `org.bouncycastle:bcprov-jdk18on` from 1.78.1 to 1.79
- [Changelog](https://github.com/bcgit/bc-java/blob/main/docs/releasenotes.html)
- [Commits](https://github.com/bcgit/bc-java/commits)

Updates `org.bouncycastle:bcpkix-jdk18on` from 1.78.1 to 1.79
- [Changelog](https://github.com/bcgit/bc-java/blob/main/docs/releasenotes.html)
- [Commits](https://github.com/bcgit/bc-java/commits)

---
updated-dependencies:
- dependency-name: org.bouncycastle:bcprov-jdk18on
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: org.bouncycastle:bcpkix-jdk18on
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-22 09:06:30 +00:00
dependabot[bot]
888aec5701 Bump io.micrometer:micrometer-core from 1.13.6 to 1.14.1 (#2253)
Bumps [io.micrometer:micrometer-core](https://github.com/micrometer-metrics/micrometer) from 1.13.6 to 1.14.1.
- [Release notes](https://github.com/micrometer-metrics/micrometer/releases)
- [Commits](https://github.com/micrometer-metrics/micrometer/compare/v1.13.6...v1.14.1)

---
updated-dependencies:
- dependency-name: io.micrometer:micrometer-core
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-22 09:06:16 +00:00
dependabot[bot]
92e7e85e77 Bump gradle from 8.7-jdk17 to 8.11-jdk17 (#2269)
* Bump gradle from 8.7-jdk17 to 8.11-jdk17

Bumps gradle from 8.7-jdk17 to 8.11-jdk17.

---
updated-dependencies:
- dependency-name: gradle
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

* Update gradle-wrapper.properties

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2024-11-22 09:02:22 +00:00
github-actions[bot]
3bf467e4ff 📝 Update README: Translation Progress Table (#2283)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-11-22 08:45:22 +00:00
yusif043-bit
dc1887db4d Translation az (#2287)
* Azerbaijani flag and dropdown item added

* Azerbaijani Language file Added

* AZ - ignore_translation.toml init

* AZ Translation Enterprise Edition Section

* Translation for Generic

* translation-az pipeline

* Translation for Analytics

* Translation for NAVBAR

* Translation for SETTINGS

* translation-az homepage

* Translation for #login

* Translation for (showJS)

* Translation for #showJS

* Translation for #PDFToWord

* Translation for #PDFToPresentation

* Translation for #PDFToText

* Translation for #PDFToHTML

* Translation for #PDFToXML

* Translation for #PDFToCSV

* Translation for #repair

* Translation for #pageLayout

* Translation for #pdfToSinglePage

* Translation for #pageExtracter

* Translation for #getPdfInfo

* Translation for #markdown-to-pdf

* Translation for #PDFToXML

* Translation for #html-to-pdf

* Translation for #PDFToHTML

* Translation for #PDFToText

* Translation for #PDFToPresentation

* Translation for #PDFToWord

* Translation for #PDFToCSV

* Translation for #url-to-pdf

* Translation for #pdfToImage

* Translation for #BookToPDF

* Translation for #PDFToBook

* Translation for #autoRedact

* Translation for #Add image

* Translation for #File to PDF

* Translation for (remove-image)

* Translation for (remove-image)

* Translation for (survey)

* Translation for (licenses)

* Translation for (printFile)

* Translation for (split-bysections)

* Translation for (overlay-pdfs)

* Translation for (split-by-size-or-count)

* Translation for (addPageNumbers)

* Translation for (adjustContrast)

* Translation for (autoSplitPDF)

* Translation for (scalePages)

* Translation for (removeCertSign)

* Translation for (removeAnnotations)

* Translation for (sign)

* Translation for (flatten)

* Translation for (extractImages)

* Translation for (merge)

* Translation for (view pdf)

* Translation for (pageRemover)

* Translation for (rotate)

* az Translation for replace-invert-color

* az Translation for addstamprequest

* Translation for (remove-image)

* Translation for #url-to-pdf

* Update messages_az_AZ.properties

* Update README.md

* Update README.md

* Update messages_az_AZ.properties

---------

Co-authored-by: NureddinFarzaliyev <nureddin.fa@gmail.com>
Co-authored-by: Lucifer25x <lucifer25x@protonmail.com>
Co-authored-by: islamd7 <vusal04999@gmail.com>
Co-authored-by: Valida Rahmanova <validerehmanova04@gmail.com>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2024-11-22 08:44:48 +00:00
albanobattistella
bab2052a60 Update messages_it_IT.properties (#2289)
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2024-11-22 08:37:31 +00:00
Ludy
7773df7443 Fix: Convert a single string in the array to a list array (#2293) 2024-11-21 21:18:41 +00:00
Ludy
32d575b4e9 try to reduce the permission; update only translation files (#2291) 2024-11-21 20:00:12 +00:00
reecebrowne
4ebeedc028 Hover tools tooltips (#2290)
* Multi-tool advert on pages that share functionality

* Update translation files

Signed-off-by: GitHub Action <action@github.com>

* Rtl CSS

* Upgraded tooltips on multitool. Order selected pages list. Repositionicons. Minor additional tweaks

* restore gb translations

* Update translation files

Signed-off-by: GitHub Action <action@github.com>

* remove blankspace

* Restore hover tooltips

* Update translation files

Signed-off-by: GitHub Action <action@github.com>

---------

Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-11-21 19:33:19 +00:00
reecebrowne
b4b005bc2e Feature/ux improvements (#2288)
* Multi-tool advert on pages that share functionality

* Update translation files

Signed-off-by: GitHub Action <action@github.com>

* Rtl CSS

* Upgraded tooltips on multitool. Order selected pages list. Repositionicons. Minor additional tweaks

* restore gb translations

* Update translation files

Signed-off-by: GitHub Action <action@github.com>

* remove blankspace

---------

Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-11-21 17:34:50 +00:00
Omar Ahmed Hassan
b92bcfe915 Update Arabic language (#2282) 2024-11-21 14:54:05 +00:00
Omar Ahmed Hassan
aeca2b23d9 Fix: Expand and de-clutter menus for matching search results in homepage #2264 (#2277)
* Hide empty menus that don't match search criteria

- Hide empty menus (accordions/feature groups) that don't match search criteria.

* Expand menus automatically for matching search results

- Fix a bug where menus (accordions/feature groups) did not automatically expand on the homepage when search results matched.

---------

Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2024-11-21 14:24:45 +00:00
Anthony Stirling
68349c4426 Update downloader.js 2024-11-21 11:47:47 +00:00
Ludy
0f6d5e5a41 Note for PR creators added (#2279) 2024-11-21 11:31:32 +00:00
Ludy
df1c5476d9 Update German language (#2276) 2024-11-20 19:45:44 +00:00
Anthony Stirling
d0d6a70250 Metrics changes (#2273)
* Update downloader.js

* Update downloader.js

* Update common.html

* Update downloader.js
2024-11-20 10:32:44 +00:00
Anthony Stirling
a6ae3734ca Frooodle patch 8 (#2275)
* Update check_properties.yml

* Update messages_en_GB.properties

* Update messages_en_GB.properties
2024-11-20 09:44:51 +00:00
Anthony Stirling
c239d95131 Update PR-Demo-cleanup.yml 2024-11-20 08:41:29 +00:00
Anthony Stirling
d591874da6 Update PR-Demo-cleanup.yml 2024-11-20 08:28:51 +00:00
Anthony Stirling
6c623d8d84 Update MetricsAggregatorService.java (#2272) 2024-11-20 08:20:01 +00:00
Rafael Encinas
e059caa14e Fix id typo for "cropPdfCanvas" querySelector (#2271)
Fix id typo
2024-11-20 07:53:14 +00:00
dependabot[bot]
8eab35761d Bump org.projectlombok:lombok from 1.18.34 to 1.18.36 (#2266)
Bumps [org.projectlombok:lombok](https://github.com/projectlombok/lombok) from 1.18.34 to 1.18.36.
- [Changelog](https://github.com/projectlombok/lombok/blob/master/doc/changelog.markdown)
- [Commits](https://github.com/projectlombok/lombok/compare/v1.18.34...v1.18.36)

---
updated-dependencies:
- dependency-name: org.projectlombok:lombok
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-18 23:16:31 +00:00
Anthony Stirling
c43af24ffe Update PR-Demo-Comment.yml 2024-11-17 16:17:44 +00:00
Anthony Stirling
e1b3cc736c Update PR-Demo-Comment.yml 2024-11-17 15:54:50 +00:00
Anthony Stirling
0fb9e18636 Update PR-Demo-Comment.yml 2024-11-17 15:53:29 +00:00
Ludy
5e1aac0b84 Read login data from application.properties (#2263)
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2024-11-17 14:08:41 +00:00
Anthony Stirling
60bf649260 Update and rename PR-Demos.yml to PR-Demo-cleanup.yml 2024-11-17 13:43:48 +00:00
Anthony Stirling
a58696a38e Create PR-Demo-Comment.yml (#2261)
* Create PR-Demo-Comment.yml

* Update PR-Demo-Comment.yml
2024-11-17 13:32:14 +00:00
Ludy
44abc67678 shows the titles of the buttons (#2262)
* shows the titles of the buttons

* Update navbar.css
2024-11-17 12:33:41 +00:00
Anthony Stirling
d1e690ff8d Update PR-Demos.yml 2024-11-17 10:13:59 +00:00
Anthony Stirling
5dc8fa08ee Create PR-Demos.yml (#2260)
* Create PR-Demos.yml

* Update build.gradle

* Update build.gradle

* Update PR-Demos.yml

* Update PR-Demos.yml

* Update PR-Demos.yml

* Update PR-Demos.yml

* Update PR-Demos.yml

* Update PR-Demos.yml

* Update PR-Demos.yml

* Update PR-Demos.yml

* Update PR-Demos.yml
2024-11-16 22:24:00 +00:00
Anthony Stirling
db028dfe27 docker move 2024-11-16 11:31:26 +00:00
Anthony Stirling
c24c504350 Delete Jenkinsfile 2024-11-16 11:22:17 +00:00
Anthony Stirling
5dcfe64d1c Update README.md 2024-11-16 11:11:12 +00:00
Anthony Stirling
d843696703 Update README.md 2024-11-16 11:07:54 +00:00
Dimitris Kaitantzidis
67de8a9460 Fix canvas pdf to csv (#2228)
* WIP: fixes canvas and rect to crop - small problem in smaller screens - neew to fix re render page on resize

* Closes #2209

* Closes #2227
2024-11-16 11:02:20 +00:00
Anthony Stirling
b26aa3417e Update build.gradle 2024-11-16 11:01:44 +00:00
Renan
8dfb5940ca Fixing bug: Add Image makes random changes to image (#2246) (#2256)
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2024-11-16 08:55:40 +00:00
Anthony Stirling
0ce479e1e3 Update push-docker.yml 2024-11-16 08:43:42 +00:00
Anthony Stirling
cca3b6b525 Update multi-tool.html 2024-11-16 08:40:34 +00:00
github-actions[bot]
03529567ba 📝 Update README: Translation Progress Table (#2254)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-11-15 22:18:01 +00:00
Anthony Stirling
781a52c759 Update ignore_translation.toml 2024-11-15 22:15:36 +00:00
Anthony Stirling
be2c103065 Update build.gradle 2024-11-15 21:56:57 +00:00
albanobattistella
80fd2eff5f Update messages_it_IT.properties (#2250)
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2024-11-15 21:53:39 +00:00
github-actions[bot]
65abfd9c7a Update translation files (#2252)
Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: GitHub Action <action@github.com>
2024-11-15 21:52:28 +00:00
Rafael Encinas
1833d7cd73 Clear file inputs after jobs (#2248) 2024-11-15 20:21:23 +00:00
reecebrowne
fd93dad9a5 Multitool advertising (#2247)
* Multi-tool advert on pages that share functionality

* Update translation files

Signed-off-by: GitHub Action <action@github.com>

---------

Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: GitHub Action <action@github.com>
2024-11-15 18:57:51 +00:00
albanobattistella
ef18b17890 Update messages_it_IT.properties (#2239) 2024-11-15 11:27:02 +00:00
Ludy
d3ae9f9a81 Prohibit the registration of unauthorized usernames (#2240) 2024-11-15 09:36:59 +00:00
Ludy
4a70d680a4 added title display on hovering, added missing german translations (#2237)
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2024-11-15 09:28:37 +00:00
Ludy
82ebd3dba9 Add: missing swagger Tag (#2238) 2024-11-15 09:25:17 +00:00
MaratheHarshad
15848e3de6 Fix: Ensure backend receives false when checkbox is unchecked in split-pdf-by-chapters feature (#2234)
* Implemented hidden input tags to resolve issue with file input handling

* Cleanup: Remove log statements for production readiness

---------

Co-authored-by: Harshad Marathe <harshad@DESKTOP-1MNKUHA>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2024-11-14 21:46:24 +00:00
github-actions[bot]
ea0d9301ff 📝 Update README: Translation Progress Table (#2236)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-11-14 20:22:45 +00:00
reecebrowne
b27e1f254c Feature/1976/multi tool multiple pages (#2200)
* Multitool - Select multiple pages for rotation tool

* Multitool multi select delete feature

* Multitool multi select UI improvements and big fixes

* Multitool multi select select all and UI improvements

* Multi tool multi select, download selected, clean up and bug fixes

* Comments

* Update buttons for page selection

* Update translation files

Signed-off-by: GitHub Action <action@github.com>

* Multitool multiselect split functionality and UI updates

* Download selected button, additional tooltips

* Update translation files

Signed-off-by: GitHub Action <action@github.com>

* revert CertSignController

* remove material icons

* restore to previous certsigncontroller

* Update CertSignController.java

---------

Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2024-11-14 20:00:36 +00:00
Anthony Stirling
7f30882e5e Setup new docker org stirlingtools/stirling-pdf (#2232)
* Update push-docker.yml

* Update push-docker.yml

* Update push-docker.yml
2024-11-14 11:20:17 +00:00
Anthony Stirling
26c0a92e30 Update pull_request_template.md 2024-11-13 13:59:22 +00:00
Renan
5cf53e39d0 Increase watermark coverage to fill page (#2049) (#2220)
* Increase watermark coverage to fill page (#2049)

* Increase watermark coverage to fill page with the new calculation (#2049)
2024-11-13 11:12:30 +00:00
Dimitris Kaitantzidis
7f566d5de8 Fix canvas crop (#2221)
* WIP: fixes canvas and rect to crop - small problem in smaller screens - neew to fix re render page on resize

* Closes #2209
2024-11-13 10:35:02 +00:00
S. Neuhaus
caa32c5bae Mention HTTP error 413 in FAQ (#2226) 2024-11-13 10:33:27 +00:00
Ludy
41c41cc88c adds missing dependencies in the endpoints (#2224) 2024-11-13 08:54:11 +00:00
leo-jmateo
c2acd74447 Catalan Translation - Stirling PDF String Updates (#2222)
* Update messages_ca_CA.properties

Partial Catalan Translation Contribution for Stirling PDF

Hi,

I’ve completed a partial Catalan translation for Stirling PDF, covering all strings up to the Pipeline section. I focused on maintaining consistency in terminology to ensure a smooth user experience in Catalan.

* Update messages_ca_CA.properties

Update on Catalan Translation Verification – Test 2 Passed

Hi [Developer’s Name],

I’ve now completed the verification for Test 2 and ensured that all keys in messages_en_GB.properties align with those in messages_ca_CA.properties. The files should now be fully synchronized with no missing or extra keys.

I’ll proceed to re-run the tests to confirm everything is in order.

Please feel free to review the updated pull request, and let me know if there’s anything further you’d like me to adjust.

Thank you for your support!

Best regards,

* Catalan Translation - Stirling PDF String Updates

Hi,

I have worked on the Catalan translation for some of the text strings in the Stirling PDF project. Attached are my contributions, which include the relevant strings for various parts of the system. I’ve made a few small adjustments to ensure the translation is as accurate and coherent as possible in technical contexts.

Changes made:
	1.	Translation of strings related to PDF manipulation tools (e.g., Add Watermark, Split PDF, etc.).
	2.	Adjustments of terms for better accuracy, such as using “Eliminar” instead of “Treure” or “Esborrar”.
	3.	Review of technical translations to ensure consistency, such as using “Nombre” instead of “Quantitat” for referring to the number of documents or pages.

Attached are the modified strings for your review:
	•	[Attach the modified strings file]

If you have any questions or need further adjustments, I’m happy to help.

Thank you for your attention and for all your work on the project!

Best regards,

* Catalan Translation - Stirling PDF String Updates

Hi,

I have worked on the Catalan translation for some of the text strings in the Stirling PDF project. Attached are my contributions, which include the relevant strings for various parts of the system. I’ve made a few small adjustments to ensure the translation is as accurate and coherent as possible in technical contexts.

Changes made:
	1.	Translation of strings related to PDF manipulation tools (e.g., Add Watermark, Split PDF, etc.).
	2.	Adjustments of terms for better accuracy, such as using “Eliminar” instead of “Treure” or “Esborrar”.
	3.	Review of technical translations to ensure consistency, such as using “Nombre” instead of “Quantitat” for referring to the number of documents or pages.

Attached are the modified strings for your review:
	•	[Attach the modified strings file]

If you have any questions or need further adjustments, I’m happy to help.

Thank you for your attention and for all your work on the project!

Best regards,

* Catalan Translation - Stirling PDF String Updates

Hi,

I have worked on the Catalan translation for some of the text strings in the Stirling PDF project. Attached are my contributions, which include the relevant strings for various parts of the system. I’ve made a few small adjustments to ensure the translation is as accurate and coherent as possible in technical contexts.

Changes made:
	1.	Translation of strings related to PDF manipulation tools (e.g., Add Watermark, Split PDF, etc.).
	2.	Adjustments of terms for better accuracy, such as using “Eliminar” instead of “Treure” or “Esborrar”.
	3.	Review of technical translations to ensure consistency, such as using “Nombre” instead of “Quantitat” for referring to the number of documents or pages.

Attached are the modified strings for your review:
	•	[Attach the modified strings file]

If you have any questions or need further adjustments, I’m happy to help.

Thank you for your attention and for all your work on the project!

Best regards,

* Catalan Translation - Stirling PDF String Updates

* 📝 Sync README
> Made via sync_files.yml

---------

Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-11-13 07:54:49 +00:00
Ludy
4d5d0e3cef Removes references to nonexistent endpoint (#2223) 2024-11-13 07:51:47 +00:00
MaratheHarshad
df6af8766f Restricting file input to .md files for Markdown to PDF conversion (#2219)
Co-authored-by: Harshad Marathe <harshad@DESKTOP-1MNKUHA>
2024-11-12 16:58:51 +00:00
Anthony Stirling
0dd4456ae8 Update HowToUseOCR.md 2024-11-12 13:31:34 +00:00
Anthony Stirling
b0c8912742 Update README.md 2024-11-12 11:06:04 +00:00
github-actions[bot]
467be09749 📝 Update README: Translation Progress Table (#2214)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-11-11 23:16:05 +00:00
Anthony Stirling
ceabcf2b3d Update get-info-on-pdf.html #2212 2024-11-11 23:12:57 +00:00
leo-jmateo
361a0c9be8 Update messages_ca_CA.properties (#2210)
* Update messages_ca_CA.properties

Partial Catalan Translation Contribution for Stirling PDF

Hi,

I’ve completed a partial Catalan translation for Stirling PDF, covering all strings up to the Pipeline section. I focused on maintaining consistency in terminology to ensure a smooth user experience in Catalan.

* Update messages_ca_CA.properties

Update on Catalan Translation Verification – Test 2 Passed

Hi [Developer’s Name],

I’ve now completed the verification for Test 2 and ensured that all keys in messages_en_GB.properties align with those in messages_ca_CA.properties. The files should now be fully synchronized with no missing or extra keys.

I’ll proceed to re-run the tests to confirm everything is in order.

Please feel free to review the updated pull request, and let me know if there’s anything further you’d like me to adjust.

Thank you for your support!

Best regards,

---------

Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2024-11-11 22:38:54 +00:00
Ludy
128ca8e224 Fix: Reading the username based on the login method. (#2211) 2024-11-11 11:55:46 +00:00
Anthony Stirling
7d1d6d1f12 Update Version-groups.md 2024-11-11 10:40:27 +00:00
Ludovic Ortega
645c786d95 feat: move helm chart to https://github.com/Stirling-Tools/Stirling-PDF-chart (#2208)
* feat: remove helm chart

Signed-off-by: Ludovic Ortega <ludovic.ortega@adminafk.fr>

* feat: mention kubernetes in install doc

Signed-off-by: Ludovic Ortega <ludovic.ortega@adminafk.fr>

---------

Signed-off-by: Ludovic Ortega <ludovic.ortega@adminafk.fr>
2024-11-10 13:48:58 +00:00
Ludy
862a88e2e9 Fix: missing opener for View PDF #2206 (#2207)
Fix missing opener for View PDF #2206
2024-11-10 13:24:12 +00:00
github-actions[bot]
2f92aa90ef 💾 Update Version (#2205)
💾 Sync Versions
> Made via sync_files.yml

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-11-09 23:11:42 +00:00
Anthony Stirling
ba8dd04086 Update build.gradle 2024-11-09 23:11:10 +00:00
github-actions[bot]
c13509cf67 💾 Update Version (#2204)
💾 Sync Versions
> Made via sync_files.yml

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-11-09 23:10:30 +00:00
Anthony Stirling
0ab02e6ceb Update build.gradle 2024-11-09 23:02:12 +00:00
Anthony Stirling
af52652aee Rename release-helm-charts.yml to release-helm-charts.yml-disabled 2024-11-09 23:01:55 +00:00
Anthony Stirling
e534f022f5 Rename lint-helm-charts.yml to lint-helm-charts.yml-disabled 2024-11-09 23:01:26 +00:00
Ludy
84867a7ad7 Fix: Card has no favorite icon (#2203)
fixes the bug if the card has no favorite icon
2024-11-09 15:07:51 +00:00
Renan
e97cb9d49e Add option to insert blank page between pages in Multi-tool (#2194) (#2201) 2024-11-08 22:51:03 +00:00
Anthony Stirling
1b0c1b6cff Searchbar in nav auto select, and exe nolonger disable CLI (#2197)
* fix remmeber me

* remove uselss comment

* Update translation files (#2185)

Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: GitHub Action <action@github.com>

* exe no longer disable CLI

---------

Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: a <a>
2024-11-07 21:50:47 +00:00
Rafael Encinas
7eea7fb3cb [Feature] Set Executor Instances limits dynamically from properties (#2193)
* Update 'ProcessExecutor.java' to use dynamic process limits from properties

* Move limits location out of 'application.properties'

* Rename 'SemaphoreLimit' to 'SessionLimit' and bundle with 'Timeout...' into one parent class
2024-11-07 00:43:57 +00:00
github-actions[bot]
c921b5d76f 📝 Update README: Translation Progress Table (#2190)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-11-05 23:28:15 +00:00
Peter Dave Hello
26ec0c5d77 Update and improve zh_TW Traditional Chinese locale (#2188) 2024-11-05 23:26:26 +00:00
ninjat
404e31468e Added input sanitization to fix self-xss issue (#2189) 2024-11-05 21:44:24 +00:00
Anthony Stirling
0c0f61aa0d fix remmeber me (#2184)
* fix remmeber me

* remove uselss comment

* Update translation files (#2185)

Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: GitHub Action <action@github.com>

---------

Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: GitHub Action <action@github.com>
2024-11-05 14:31:31 +00:00
Ludovic Ortega
40ffb6559d feat: add helm chart github action (#2113)
* feat: add helm chart github action

Signed-off-by: Ludovic Ortega <ludovic.ortega@adminafk.fr>

* fix: remove test branch

Signed-off-by: Ludovic Ortega <ludovic.ortega@adminafk.fr>

* fix: run helm-docs-built after syncing version

Signed-off-by: Ludovic Ortega <ludovic.ortega@adminafk.fr>

* fix: helm repo url

---------

Signed-off-by: Ludovic Ortega <ludovic.ortega@adminafk.fr>
2024-11-04 20:13:26 +00:00
github-actions[bot]
645c5ff36f 📝 Update README: Translation Progress Table (#2165)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-11-03 22:47:28 +00:00
MaratheHarshad
83db7a22f5 Fix: Navbar layout overflow (#2162)
Fix: Navbar layout overflow using Bootstrap class .navbar-expand-xl

Co-authored-by: Harshad Marathe <harshad@DESKTOP-1MNKUHA>
2024-11-03 20:59:57 +00:00
Ludy
ebfccfa835 Corrects AI generated translation (#2166) 2024-11-03 20:39:00 +00:00
Saud Fatayerji
aa810163d2 Completed translations for 19 languages using AI (#2164)
Created translations for various languages using AI
2024-11-03 20:14:45 +00:00
github-actions[bot]
dcb69ad66a 📝 Update README: Translation Progress Table (#2160)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-11-03 18:05:03 +00:00
Ludy
1a19024961 Fix: Auto language detection #2122 (#2148)
* Fix: Auto language detection #2122

* add LanguageService and AdditionalLanguageJsController

* hidden swagger
2024-11-03 14:20:26 +00:00
albanobattistella
68c9601245 Update messages_it_IT.properties (#2161) 2024-11-03 14:16:06 +00:00
Ludy
7ec343d9ce Fix: Add missing .map file for minified files (#2156) 2024-11-03 07:55:39 +00:00
Ludy
6f42d976f6 Fix: Path correction to draggable.js #2154 + little makeup (#2159) 2024-11-03 07:26:45 +00:00
Ludy
cf13803fd4 Fix: redeclaration of const and add: tranlation placeholder for Session Expiry Messages (#2158)
Fix: redeclaration of const
2024-11-03 07:24:16 +00:00
Ludy
a8d0d1a871 re-config labeler & add new labels (#2153)
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2024-11-03 07:21:56 +00:00
Ludy
a5aac01b4d fixed minor bugs in Markdown (#2152) 2024-11-03 07:20:10 +00:00
albanobattistella
2be14788b1 Update messages_it_IT.properties (#2146) 2024-11-01 16:32:52 +00:00
github-actions[bot]
217404be7f 📝 Update README: Translation Progress Table (#2136)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-31 20:33:03 +00:00
github-actions[bot]
d3dc3e07b2 Update translation files (#2145)
Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: GitHub Action <action@github.com>
2024-10-31 20:22:11 +00:00
Eric
94702dbafa fix signature logo not loading and add option to disable it (#2143)
* fix signature logo not loading and add option to disable it

* Hardening suggestions for Stirling-PDF / fix-sig-logo (#2144)

Modernize and secure temp file creation

Co-authored-by: pixeebot[bot] <104101892+pixeebot[bot]@users.noreply.github.com>

---------

Co-authored-by: pixeebot[bot] <104101892+pixeebot[bot]@users.noreply.github.com>
2024-10-31 20:18:42 +00:00
Anthony Stirling
febc3cf48b Update pull_request_template.md 2024-10-31 17:46:30 +00:00
Philip H.
c5abb47403 navbar.css: prevent overlapping of elements (#2140)
go-pro-link is overlapping the settings button
2024-10-31 17:45:44 +00:00
Anthony Stirling
0e3c9bcc10 Update README.md 2024-10-31 14:52:41 +00:00
github-actions[bot]
384c3ee88f 💾 Update Version (#2139)
💾 Sync Versions
> Made via sync_files.yml

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-31 13:06:55 +00:00
Anthony Stirling
5f7a0537f9 Update build.gradle 2024-10-31 13:06:12 +00:00
Anthony Stirling
5aa5628465 [bug fix] Update compress-pdf.html (#2138)
Update compress-pdf.html
2024-10-31 10:59:51 +00:00
albanobattistella
0d91bca932 Update messages_it_IT.properties (#2135) 2024-10-30 19:55:54 +00:00
212 changed files with 41001 additions and 7236 deletions

View File

@@ -16,21 +16,27 @@ Java:
Back End:
- changed-files:
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/config/security/**/*'
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/config/model/provider/**/*'
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/config/**/*'
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/controller/**/*'
- any-glob-to-any-file: 'src/main/resources/settings.yml.template'
- any-glob-to-any-file: 'src/main/resources/application.properties'
- any-glob-to-any-file: 'src/main/resources/banner.txt'
- any-glob-to-any-file: 'scripts/png_to_webp.py'
- any-glob-to-any-file: 'split_photos.py'
Security:
- changed-files:
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/config/security/**/*'
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/config/model/provider/**/*'
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/config/model/AuthenticationType.java'
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/model/provider/**/*'
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/model/AuthenticationType.java'
- any-glob-to-any-file: 'scripts/download-security-jar.sh'
API:
- changed-files:
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/controller/web/MetricsController.java'
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/controller/api/**/*'
- any-glob-to-any-file: 'scripts/png_to_webp.py'
- any-glob-to-any-file: 'split_photos.py'
Documentation:
- changed-files:
@@ -43,6 +49,9 @@ Docker:
- any-glob-to-any-file: 'Dockerfile'
- any-glob-to-any-file: 'Dockerfile-*'
- any-glob-to-any-file: 'exampleYmlFiles/*.yml'
- any-glob-to-any-file: 'scripts/init.sh'
- any-glob-to-any-file: 'scripts/init-without-ocr.sh'
- any-glob-to-any-file: 'scripts/installFonts.sh'
Test:
- changed-files:

21
.github/labels.yml vendored
View File

@@ -3,9 +3,12 @@
#
# The repository labels will be automatically configured using this file and
# the GitHub Action https://github.com/marketplace/actions/github-labeler.
- name: "Licenses"
color: "EDEDED"
from_name: "licenses"
- name: "Back End"
color: "20CE6C"
description: "Issues related to back-end development"
description: "Issues or pull requests related to back-end development"
from_name: "Back end"
- name: "Bug"
description: "Something isn't working"
@@ -24,6 +27,7 @@
from_name: "documentation"
- name: "Done for next release"
color: "0CDBD1"
description: "Items that are completed and will be included in the next release"
- name: "Done"
color: "60F13B"
- name: "duplicate"
@@ -37,7 +41,7 @@
description: "Fix needs to be confirmed"
- name: "Front End"
color: "BBD2F1"
description: "Issues related to front-end development"
description: "Issues or pull requests related to front-end development"
- name: "github-actions"
description: "Pull requests that update GitHub Actions code"
color: "999999"
@@ -91,3 +95,16 @@
description: "Testing-related issues or pull requests"
- name: "Stale"
color: "000000"
description: "Issues or pull requests that have become inactive"
- name: "Priority: Critical"
color: "000000"
description: "Issues or pull requests with the highest priority"
- name: "Priority: High"
color: "FF0000"
description: "Issues or pull requests with high priority"
- name: "Priority: Medium"
color: "FFFF00"
description: "Issues or pull requests with medium priority"
- name: "Priority: Low"
color: "00FF00"
description: "Issues or pull requests with low priority"

View File

@@ -8,6 +8,8 @@ Closes #(issue_number)
- [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have performed a self-review of my own code
- [ ] I have attached images of the change if it is UI based
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] If my code has heavily changed functionality I have updated relevant docs on [Stirling-PDFs doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
- [ ] My changes generate no new warnings
- [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only)

View File

@@ -9,8 +9,9 @@ The script also provides functionality to update the translation files to match
adjusting the format.
Usage:
python script_name.py --reference-file <path_to_reference_file> --branch <branch_name> [--files <list_of_changed_files>]
python check_language_properties.py --reference-file <path_to_reference_file> --branch <branch_name> [--actor <actor_name>] [--files <list_of_changed_files>]
"""
import copy
import glob
import os
@@ -18,6 +19,10 @@ import argparse
import re
# Maximum size for properties files (e.g., 200 KB)
MAX_FILE_SIZE = 200 * 1024
def parse_properties_file(file_path):
"""Parses a .properties file and returns a list of objects (including comments, empty lines, and line numbers)."""
properties_list = []
@@ -95,7 +100,7 @@ def write_json_file(file_path, updated_properties):
def update_missing_keys(reference_file, file_list, branch=""):
reference_properties = parse_properties_file(reference_file)
for file_path in file_list:
basename_current_file = os.path.basename(branch + file_path)
basename_current_file = os.path.basename(os.path.join(branch, file_path))
if (
basename_current_file == os.path.basename(reference_file)
or not file_path.endswith(".properties")
@@ -103,7 +108,7 @@ def update_missing_keys(reference_file, file_list, branch=""):
):
continue
current_properties = parse_properties_file(branch + file_path)
current_properties = parse_properties_file(os.path.join(branch, file_path))
updated_properties = []
for ref_entry in reference_properties:
ref_entry_copy = copy.deepcopy(ref_entry)
@@ -114,60 +119,79 @@ def update_missing_keys(reference_file, file_list, branch=""):
if ref_entry_copy["key"] == current_entry["key"]:
ref_entry_copy["value"] = current_entry["value"]
updated_properties.append(ref_entry_copy)
write_json_file(branch + file_path, updated_properties)
write_json_file(os.path.join(branch, file_path), updated_properties)
def check_for_missing_keys(reference_file, file_list, branch):
update_missing_keys(reference_file, file_list, branch + "/")
update_missing_keys(reference_file, file_list, branch)
def read_properties(file_path):
with open(file_path, "r", encoding="utf-8") as file:
return file.read().splitlines()
if os.path.isfile(file_path) and os.path.exists(file_path):
with open(file_path, "r", encoding="utf-8") as file:
return file.read().splitlines()
return [""]
def check_for_differences(reference_file, file_list, branch):
def check_for_differences(reference_file, file_list, branch, actor):
reference_branch = reference_file.split("/")[0]
basename_reference_file = os.path.basename(reference_file)
report = []
report.append(
f"### 📋 Checking with the file `{basename_reference_file}` from the `{reference_branch}` - Checking the `{branch}`"
)
report.append(f"#### 🔄 Reference Branch: `{reference_branch}`")
reference_lines = read_properties(reference_file)
has_differences = False
only_reference_file = True
for file_path in file_list:
basename_current_file = os.path.basename(branch + "/" + file_path)
file_arr = file_list
if len(file_list) == 1:
file_arr = file_list[0].split()
base_dir = os.path.abspath(os.path.join(os.getcwd(), "src", "main", "resources"))
for file_path in file_arr:
absolute_path = os.path.abspath(file_path)
# Verify that file is within the expected directory
if not absolute_path.startswith(base_dir):
raise ValueError(f"Unsafe file found: {file_path}")
# Verify file size before processing
if os.path.getsize(os.path.join(branch, file_path)) > MAX_FILE_SIZE:
raise ValueError(
f"The file {file_path} is too large and could pose a security risk."
)
basename_current_file = os.path.basename(os.path.join(branch, file_path))
if (
basename_current_file == basename_reference_file
or not file_path.startswith(
os.path.join("src", "main", "resources", "messages_")
)
or not file_path.endswith(".properties")
or not basename_current_file.startswith("messages_")
):
continue
only_reference_file = False
report.append(f"#### 🗂️ **Checking File:** `{basename_current_file}`...")
current_lines = read_properties(branch + "/" + file_path)
report.append(f"#### 📃 **File Check:** `{basename_current_file}`")
current_lines = read_properties(os.path.join(branch, file_path))
reference_line_count = len(reference_lines)
current_line_count = len(current_lines)
if reference_line_count != current_line_count:
report.append("")
report.append("- **Test 1 Status:** ❌ Failed")
report.append("1. **Test Status:** ❌ **_Failed_**")
report.append(" - **Issue:**")
has_differences = True
if reference_line_count > current_line_count:
report.append(
f" - **Issue:** Missing lines! Comments, empty lines, or translation strings are missing. Details: {reference_line_count} (reference) vs {current_line_count} (current)."
f" - **_Mismatched line count_**: {reference_line_count} (reference) vs {current_line_count} (current). Comments, empty lines, or translation strings are missing."
)
elif reference_line_count < current_line_count:
report.append(
f" - **Issue:** Too many lines! Check your translation files! Details: {reference_line_count} (reference) vs {current_line_count} (current)."
f" - **_Too many lines_**: {reference_line_count} (reference) vs {current_line_count} (current). Please verify if there is an additional line that needs to be removed."
)
# update_missing_keys(reference_file, [file_path], branch + "/")
else:
report.append("- **Test 1 Status:** ✅ Passed")
report.append("1. **Test Status:** ✅ **_Passed_**")
# Check for missing or extra keys
current_keys = []
@@ -192,32 +216,42 @@ def check_for_differences(reference_file, file_list, branch):
has_differences = True
missing_keys_str = "`, `".join(missing_keys_list)
extra_keys_str = "`, `".join(extra_keys_list)
report.append("- **Test 2 Status:** ❌ Failed")
report.append("2. **Test Status:** ❌ **_Failed_**")
report.append(" - **Issue:**")
if missing_keys_list:
spaces_keys_list = []
for key in missing_keys_list:
if " " in key:
spaces_keys_list.append(key)
if spaces_keys_list:
spaces_keys_str = "`, `".join(spaces_keys_list)
report.append(
f" - **_Keys containing unnecessary spaces_**: `{spaces_keys_str}`!"
)
report.append(
f" - **Issue:** There are keys in ***{basename_current_file}*** `{missing_keys_str}` that are not present in ***{basename_reference_file}***!"
f" - **_Extra keys in `{basename_current_file}`_**: `{missing_keys_str}` that are not present in **_`{basename_reference_file}`_**."
)
if extra_keys_list:
report.append(
f" - **Issue:** There are keys in ***{basename_reference_file}*** `{extra_keys_str}` that are not present in ***{basename_current_file}***!"
f" - **_Missing keys in `{basename_reference_file}`_**: `{extra_keys_str}` that are not present in **_`{basename_current_file}`_**."
)
# update_missing_keys(reference_file, [file_path], branch + "/")
else:
report.append("- **Test 2 Status:** ✅ Passed")
# if has_differences:
# report.append("")
# report.append(f"#### 🚧 ***{basename_current_file}*** will be corrected...")
report.append("2. **Test Status:** ✅ **_Passed_**")
report.append("")
report.append("---")
report.append("")
# update_file_list = glob.glob(branch + "/src/**/messages_*.properties", recursive=True)
# update_missing_keys(reference_file, update_file_list)
# report.append("---")
# report.append("")
if has_differences:
report.append("## ❌ Overall Check Status: **_Failed_**")
report.append("")
report.append(
f"@{actor} please check your translation if it conforms to the standard. Follow the format of [messages_en_GB.properties](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/src/main/resources/messages_en_GB.properties)"
)
else:
report.append("## ✅ Overall Check Status: **_Success_**")
report.append("")
report.append(
f"Thanks @{actor} for your help in keeping the translations up to date."
)
if not only_reference_file:
print("\n".join(report))
@@ -225,6 +259,11 @@ def check_for_differences(reference_file, file_list, branch):
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Find missing keys")
parser.add_argument(
"--actor",
required=False,
help="Actor from PR.",
)
parser.add_argument(
"--reference-file",
required=True,
@@ -244,11 +283,21 @@ if __name__ == "__main__":
)
args = parser.parse_args()
# Sanitize --actor input to avoid injection attacks
if args.actor:
args.actor = re.sub(r"[^a-zA-Z0-9_\\-]", "", args.actor)
# Sanitize --branch input to avoid injection attacks
if args.branch:
args.branch = re.sub(r"[^a-zA-Z0-9\\-]", "", args.branch)
file_list = args.files
if file_list is None:
file_list = glob.glob(
os.getcwd() + "/src/**/messages_*.properties", recursive=True
os.path.join(
os.getcwd(), "src", "main", "resources", "messages_*.properties"
)
)
update_missing_keys(args.reference_file, file_list)
else:
check_for_differences(args.reference_file, file_list, args.branch)
check_for_differences(args.reference_file, file_list, args.branch, args.actor)

View File

@@ -1,100 +0,0 @@
import re
import yaml
# Paths to the files
chart_yaml_path = "chart/stirling-pdf/Chart.yaml"
gradle_path = "build.gradle"
def get_chart_version(path):
"""
Reads the version and the appVersion from Chart.yaml.
Args:
path (str): The file path to the Chart.yaml.
Returns:
dict: The version under "chart" key and the appVersion under "app" key.
"""
with open(path, encoding="utf-8") as file:
chart_yaml = yaml.safe_load(file)
return {
"chart": chart_yaml["version"],
"app": chart_yaml["appVersion"]
}
def get_gradle_version(path):
"""
Extracts the version from build.gradle.
Args:
path (str): The file path to the build.gradle.
Returns:
str: The version if found, otherwise an empty string.
"""
with open(path, encoding="utf-8") as file:
for line in file:
if "version =" in line:
# Extracts the value after 'version ='
return re.search(r'version\s*=\s*[\'"](.+?)[\'"]', line).group(1)
return ""
def get_new_chart_version(chart_version, old_app_version, new_app_version):
"""
Get the new chart version from
Args:
str: The current chart version.
str: The current app version.
str: The new app version.
Returns:
str: The new chart version to update to.
"""
chart_major, chart_minor, chart_patch = chart_version.split(".")
old_major, old_minor, old_patch = old_app_version.split(".")
new_major, new_minor, new_patch = new_app_version.split(".")
if old_major != new_major:
new_chart_version = f"{int(chart_major)+1}.0.0"
elif old_minor != new_minor:
new_chart_version = f"{chart_major}.{int(chart_minor)+1}.0"
elif old_patch != new_patch:
new_chart_version = f"{chart_major}.{chart_minor}.{int(chart_patch)+1}"
return new_chart_version
def update_chart_version(path, new_chart_version, new_app_version):
"""
Updates the version and the appVersion in Chart.yaml with a new version.
Args:
path (str): The file path to the Chart.yaml.
new_chart_version (str): The new chart version to update to.
new_app_version (str): The new app version to update to.
"""
with open(path, encoding="utf-8") as file:
chart_yaml = yaml.safe_load(file)
chart_yaml["version"] = new_chart_version
chart_yaml["appVersion"] = new_app_version
with open(path, "w", encoding="utf-8") as file:
yaml.safe_dump(chart_yaml, file)
# Main logic
chart_version = get_chart_version(chart_yaml_path)
gradle_version = get_gradle_version(gradle_path)
if chart_version["app"] != gradle_version:
new_chart_version = get_new_chart_version(chart_version["chart"], chart_version["app"], gradle_version, )
print(
f"Versions do not match. Updating Chart.yaml from {chart_version['chart']} to {new_chart_version}."
)
update_chart_version(chart_yaml_path, new_chart_version, gradle_version)
else:
print("Versions match. No update required.")

179
.github/workflows/PR-Demo-Comment.yml vendored Normal file
View File

@@ -0,0 +1,179 @@
name: PR Deployment via Comment
on:
issue_comment:
types: [created]
jobs:
check-comment:
runs-on: ubuntu-latest
if: |
github.event.issue.pull_request &&
(
contains(github.event.comment.body, 'prdeploy') ||
contains(github.event.comment.body, 'deploypr')
)
&&
(
github.event.comment.user.login == 'frooodle' ||
github.event.comment.user.login == 'sf298' ||
github.event.comment.user.login == 'Ludy87' ||
github.event.comment.user.login == 'LaserKaspar' ||
github.event.comment.user.login == 'sbplat' ||
github.event.comment.user.login == 'reecebrowne'
)
outputs:
pr_number: ${{ steps.get-pr.outputs.pr_number }}
pr_repository: ${{ steps.get-pr-info.outputs.repository }}
pr_ref: ${{ steps.get-pr-info.outputs.ref }}
steps:
- name: Get PR data
id: get-pr
uses: actions/github-script@v7
with:
script: |
const prNumber = context.payload.issue.number;
console.log(`PR Number: ${prNumber}`);
core.setOutput('pr_number', prNumber);
- name: Get PR repository and ref
id: get-pr-info
uses: actions/github-script@v7
with:
script: |
const { owner, repo } = context.repo;
const prNumber = context.payload.issue.number;
const { data: pr } = await github.rest.pulls.get({
owner,
repo,
pull_number: prNumber,
});
// For forks, use the full repository name, for internal PRs use the current repo
const repository = pr.head.repo.fork ? pr.head.repo.full_name : `${owner}/${repo}`;
console.log(`PR Repository: ${repository}`);
console.log(`PR Branch: ${pr.head.ref}`);
core.setOutput('repository', repository);
core.setOutput('ref', pr.head.ref);
deploy-pr:
needs: check-comment
runs-on: ubuntu-latest
steps:
- name: Checkout PR
uses: actions/checkout@v4
with:
repository: ${{ needs.check-comment.outputs.pr_repository }}
ref: ${{ needs.check-comment.outputs.pr_ref }}
token: ${{ secrets.GITHUB_TOKEN }}
- name: Set up JDK
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
- name: Run Gradle Command
run: ./gradlew clean build
env:
DOCKER_ENABLE_SECURITY: false
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Get version number
id: versionNumber
run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_API }}
- name: Build and push PR-specific image
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile
push: true
tags: ${{ secrets.DOCKER_HUB_USERNAME }}/test:pr-${{ needs.check-comment.outputs.pr_number }}
build-args: VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
platforms: linux/amd64
- name: Set up SSH
run: |
mkdir -p ~/.ssh/
echo "${{ secrets.VPS_SSH_KEY }}" > ../private.key
sudo chmod 600 ../private.key
- name: Deploy to VPS
run: |
# First create the docker-compose content locally
cat > docker-compose.yml << 'EOF'
version: '3.3'
services:
stirling-pdf:
container_name: stirling-pdf-pr-${{ needs.check-comment.outputs.pr_number }}
image: ${{ secrets.DOCKER_HUB_USERNAME }}/test:pr-${{ needs.check-comment.outputs.pr_number }}
ports:
- "${{ needs.check-comment.outputs.pr_number }}:8080"
volumes:
- /stirling/PR-${{ needs.check-comment.outputs.pr_number }}/data:/usr/share/tessdata:rw
- /stirling/PR-${{ needs.check-comment.outputs.pr_number }}/config:/configs:rw
- /stirling/PR-${{ needs.check-comment.outputs.pr_number }}/logs:/logs:rw
environment:
DOCKER_ENABLE_SECURITY: "false"
SECURITY_ENABLELOGIN: "false"
SYSTEM_DEFAULTLOCALE: en-GB
UI_APPNAME: "Stirling-PDF PR#${{ needs.check-comment.outputs.pr_number }}"
UI_HOMEDESCRIPTION: "PR#${{ needs.check-comment.outputs.pr_number }} for Stirling-PDF Latest"
UI_APPNAMENAVBAR: "PR#${{ needs.check-comment.outputs.pr_number }}"
SYSTEM_MAXFILESIZE: "100"
METRICS_ENABLED: "true"
SYSTEM_GOOGLEVISIBILITY: "false"
restart: on-failure:5
EOF
# Then copy the file and execute commands
scp -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null docker-compose.yml ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }}:/tmp/docker-compose.yml
ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }} << 'ENDSSH'
# Create PR-specific directories
mkdir -p /stirling/PR-${{ needs.check-comment.outputs.pr_number }}/{data,config,logs}
# Move docker-compose file to correct location
mv /tmp/docker-compose.yml /stirling/PR-${{ needs.check-comment.outputs.pr_number }}/docker-compose.yml
# Start or restart the container
cd /stirling/PR-${{ needs.check-comment.outputs.pr_number }}
docker-compose pull
docker-compose up -d
ENDSSH
- name: Post deployment URL to PR
if: success()
uses: actions/github-script@v7
with:
script: |
const { GITHUB_REPOSITORY } = process.env;
const [repoOwner, repoName] = GITHUB_REPOSITORY.split('/');
const prNumber = ${{ needs.check-comment.outputs.pr_number }};
const deploymentUrl = `http://${{ secrets.VPS_HOST }}:${prNumber}`;
const commentBody = `## 🚀 PR Test Deployment\n\n` +
`Your PR has been deployed for testing!\n\n` +
`🔗 **Test URL:** [${deploymentUrl}](${deploymentUrl})\n\n` +
`This deployment will be automatically cleaned up when the PR is closed.\n\n`;
await github.rest.issues.createComment({
owner: repoOwner,
repo: repoName,
issue_number: prNumber,
body: commentBody
});

78
.github/workflows/PR-Demo-cleanup.yml vendored Normal file
View File

@@ -0,0 +1,78 @@
name: PR Deployment cleanup
on:
pull_request:
types: [opened, synchronize, reopened, closed]
permissions:
contents: write
pull-requests: write
env:
SERVER_IP: ${{ secrets.VPS_IP }} # Add this to your GitHub secrets
CLEANUP_PERFORMED: 'false' # Add flag to track if cleanup occurred
jobs:
cleanup:
runs-on: ubuntu-latest
if: github.event.action == 'closed'
steps:
- name: Set up SSH
run: |
mkdir -p ~/.ssh/
echo "${{ secrets.VPS_SSH_KEY }}" > ../private.key
sudo chmod 600 ../private.key
- name: Cleanup PR deployment
id: cleanup
run: |
CLEANUP_STATUS=$(ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }} << 'ENDSSH'
if [ -d "/stirling/PR-${{ github.event.pull_request.number }}" ]; then
echo "Found PR directory, proceeding with cleanup..."
# Stop and remove containers
cd /stirling/PR-${{ github.event.pull_request.number }}
docker-compose down || true
# Go back to root before removal
cd /
# Remove PR-specific directories
rm -rf /stirling/PR-${{ github.event.pull_request.number }}
# Remove the Docker image
docker rmi --no-prune ${{ secrets.DOCKER_HUB_USERNAME }}/test:pr-${{ github.event.pull_request.number }} || true
echo "PERFORMED_CLEANUP"
else
echo "PR directory not found, nothing to clean up"
echo "NO_CLEANUP_NEEDED"
fi
ENDSSH
)
if [[ $CLEANUP_STATUS == *"PERFORMED_CLEANUP"* ]]; then
echo "cleanup_performed=true" >> $GITHUB_OUTPUT
else
echo "cleanup_performed=false" >> $GITHUB_OUTPUT
fi
- name: Post cleanup notice to PR
if: steps.cleanup.outputs.cleanup_performed == 'true'
uses: actions/github-script@v7
with:
script: |
const { GITHUB_REPOSITORY } = process.env;
const [repoOwner, repoName] = GITHUB_REPOSITORY.split('/');
const prNumber = context.issue.number;
const commentBody = `## 🧹 Deployment Cleanup\n\n` +
`The test deployment for this PR has been cleaned up.`;
await github.rest.issues.createComment({
owner: repoOwner,
repo: repoName,
issue_number: prNumber,
body: commentBody
});

View File

@@ -6,18 +6,22 @@ on:
paths:
- "src/main/resources/messages_*.properties"
push:
branches: ["main"]
paths:
- "src/main/resources/messages_en_GB.properties"
permissions:
contents: write
pull-requests: write
jobs:
check-files:
if: github.event_name == 'pull_request_target'
runs-on: ubuntu-latest
steps:
- name: Checkout main branch first
uses: actions/checkout@v4
with:
ref: main
path: main-branch
fetch-depth: 0
- name: Checkout PR branch
uses: actions/checkout@v4
with:
@@ -26,13 +30,6 @@ jobs:
path: pr-branch
fetch-depth: 0
- name: Checkout main branch
uses: actions/checkout@v4
with:
ref: main
path: main-branch
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v5
with:
@@ -49,56 +46,73 @@ jobs:
echo "Fetching PR changed files..."
cd pr-branch
gh repo set-default ${{ github.repository }}
gh pr view ${{ github.event.pull_request.number }} --json files -q ".files[].path" > ../changed_files.txt
# Store files in a safe way, only allowing valid properties files
echo "Getting list of changed files from PR..."
gh pr view ${{ github.event.pull_request.number }} --json files -q ".files[].path" | grep -E '^src/main/resources/messages_[a-zA-Z_]+\.properties$' > ../changed_files.txt
cd ..
echo $(cat changed_files.txt)
BRANCH_PATH="pr-branch"
echo "BRANCH_PATH=${BRANCH_PATH}" >> $GITHUB_ENV
CHANGED_FILES=$(cat changed_files.txt | tr '\n' ' ')
echo "CHANGED_FILES=${CHANGED_FILES}" >> $GITHUB_ENV
echo "Changed files: ${CHANGED_FILES}"
echo "Branch: ${BRANCH_PATH}"
echo "Processing changed files..."
mapfile -t CHANGED_FILES < changed_files.txt
CHANGED_FILES_STR="${CHANGED_FILES[*]}"
echo "CHANGED_FILES=${CHANGED_FILES_STR}" >> $GITHUB_ENV
echo "Changed files: ${CHANGED_FILES_STR}"
- name: Determine reference file
id: determine-file
run: |
echo "Determining reference file..."
if echo "${{ env.CHANGED_FILES }}" | grep -q 'src/main/resources/messages_en_GB.properties'; then
if grep -Fxq "src/main/resources/messages_en_GB.properties" changed_files.txt; then
echo "Using PR branch reference file"
echo "REFERENCE_FILE=pr-branch/src/main/resources/messages_en_GB.properties" >> $GITHUB_ENV
else
echo "Using main branch reference file"
echo "REFERENCE_FILE=main-branch/src/main/resources/messages_en_GB.properties" >> $GITHUB_ENV
fi
echo "REFERENCE_FILE=${{ env.REFERENCE_FILE }}"
- name: Show REFERENCE_FILE
run: echo "Reference file is set to ${{ env.REFERENCE_FILE }}"
run: echo "Reference file is set to ${REFERENCE_FILE}"
- name: Run Python script to check files
id: run-check
run: |
python main-branch/.github/scripts/check_language_properties.py --reference-file ${{ env.REFERENCE_FILE }} --branch ${{ env.BRANCH_PATH }} --files ${{ env.CHANGED_FILES }} > failure.txt || true
echo "Running Python script to check files..."
python main-branch/.github/scripts/check_language_properties.py \
--actor ${{ github.event.pull_request.user.login }} \
--reference-file "${REFERENCE_FILE}" \
--branch pr-branch \
--files "${CHANGED_FILES[@]}" > result.txt || true
- name: Capture output
id: capture-output
run: |
if [ -f failure.txt ] && [ -s failure.txt ]; then
echo "Test failed, capturing output..."
ERROR_OUTPUT=$(cat failure.txt)
echo "ERROR_OUTPUT<<EOF" >> $GITHUB_ENV
echo "$ERROR_OUTPUT" >> $GITHUB_ENV
if [ -f result.txt ] && [ -s result.txt ]; then
echo "Test, capturing output..."
SCRIPT_OUTPUT=$(cat result.txt)
echo "SCRIPT_OUTPUT<<EOF" >> $GITHUB_ENV
echo "$SCRIPT_OUTPUT" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
echo $ERROR_OUTPUT
echo "${SCRIPT_OUTPUT}"
# Set FAIL_JOB to true if SCRIPT_OUTPUT contains ❌
if [[ "$SCRIPT_OUTPUT" == *"❌"* ]]; then
echo "FAIL_JOB=true" >> $GITHUB_ENV
else
echo "FAIL_JOB=false" >> $GITHUB_ENV
fi
else
echo "No errors found."
echo "ERROR_OUTPUT=" >> $GITHUB_ENV
echo "No update found."
echo "SCRIPT_OUTPUT=" >> $GITHUB_ENV
echo "FAIL_JOB=false" >> $GITHUB_ENV
fi
- name: Post comment on PR
if: env.ERROR_OUTPUT != ''
if: env.SCRIPT_OUTPUT != ''
uses: actions/github-script@v7
with:
script: |
const { GITHUB_REPOSITORY, ERROR_OUTPUT } = process.env;
const { GITHUB_REPOSITORY, SCRIPT_OUTPUT } = process.env;
const [repoOwner, repoName] = GITHUB_REPOSITORY.split('/');
const prNumber = context.issue.number;
@@ -120,7 +134,7 @@ jobs:
owner: repoOwner,
repo: repoName,
comment_id: comment.id,
body: `## 🚀 Translation Verification Summary\n\n\n${ERROR_OUTPUT}\n`
body: `## 🚀 Translation Verification Summary\n\n\n${SCRIPT_OUTPUT}\n`
});
console.log("Updated existing comment.");
} else if (!comment) {
@@ -129,33 +143,24 @@ jobs:
owner: repoOwner,
repo: repoName,
issue_number: prNumber,
body: `## 🚀 Translation Verification Summary\n\n\n${ERROR_OUTPUT}\n`
body: `## 🚀 Translation Verification Summary\n\n\n${SCRIPT_OUTPUT}\n`
});
console.log("Created new comment.");
} else {
console.log("Comment update attempt denied. Actor does not match.");
}
# - name: Set up git config
# run: |
# git config --global user.name "github-actions[bot]"
# git config --global user.email "github-actions[bot]@users.noreply.github.com"
# - name: Add translation keys
# run: |
# cd ${{ env.BRANCH_PATH }}
# git add src/main/resources/messages_*.properties
# git diff --staged --quiet || echo "CHANGES_DETECTED=true" >> $GITHUB_ENV
# git commit -m "Update translation files" || echo "No changes to commit"
# - name: Push
# if: env.CHANGES_DETECTED == 'true'
# run: |
# cd pr-branch
# git remote set-url origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.event.pull_request.head.repo.full_name }}.git
# git push origin ${{ github.head_ref }} || echo "Push failed: possibly no changes to push"
- name: Fail job if errors found
if: env.FAIL_JOB == 'true'
run: |
echo "Failing the job because errors were detected."
exit 1
update-translations-main:
if: github.event_name == 'push'
permissions:
contents: write
pull-requests: write
runs-on: ubuntu-latest
steps:
- name: Checkout repository
@@ -169,7 +174,10 @@ jobs:
- name: Run Python script to check files
id: run-check
run: |
python .github/scripts/check_language_properties.py --reference-file src/main/resources/messages_en_GB.properties --branch main
echo "Running Python script to check files..."
python .github/scripts/check_language_properties.py \
--reference-file src/main/resources/messages_en_GB.properties \
--branch main
- name: Set up git config
run: |
@@ -184,7 +192,7 @@ jobs:
- name: Create Pull Request
id: cpr
if: env.CHANGES_DETECTED == 'true'
uses: peter-evans/create-pull-request@v6
uses: peter-evans/create-pull-request@v7
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: "Update translation files"
@@ -193,6 +201,8 @@ jobs:
signoff: true
branch: update_translation_files
title: "Update translation files"
add-paths: |
src/main/resources/messages_*.properties
body: |
Auto-generated by [create-pull-request][1]
@@ -200,3 +210,4 @@ jobs:
labels: Translation
draft: false
delete-branch: true
sign-commits: true

View File

@@ -10,6 +10,7 @@ on:
permissions:
contents: read
packages: write
jobs:
push:
runs-on: ubuntu-latest
@@ -66,6 +67,8 @@ jobs:
images: |
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/s-pdf
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf
${{ secrets.DOCKER_HUB_ORG_USERNAME }}/stirling-pdf
tags: |
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }},enable=${{ github.ref == 'refs/heads/master' }}
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/master' }}
@@ -93,6 +96,8 @@ jobs:
images: |
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/s-pdf
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf
${{ secrets.DOCKER_HUB_ORG_USERNAME }}/stirling-pdf
tags: |
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}-ultra-lite,enable=${{ github.ref == 'refs/heads/master' }}
type=raw,value=latest-ultra-lite,enable=${{ github.ref == 'refs/heads/master' }}
@@ -119,6 +124,8 @@ jobs:
images: |
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/s-pdf
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf
${{ secrets.DOCKER_HUB_ORG_USERNAME }}/stirling-pdf
tags: |
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}-fat,enable=${{ github.ref == 'refs/heads/master' }}
type=raw,value=latest-fat,enable=${{ github.ref == 'refs/heads/master' }}

View File

@@ -14,44 +14,6 @@ permissions:
pull-requests: write
jobs:
sync-versions:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.x"
- name: Install dependencies
run: pip install pyyaml
- name: Sync versions
run: python .github/scripts/gradle_to_chart.py
- name: Set up git config
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"
- name: Run git add
run: |
git add .
git diff --staged --quiet || git commit -m ":floppy_disk: Sync Versions
> Made via sync_files.yml" || echo "no changes"
- name: Create Pull Request
uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: Update files
committer: GitHub Action <action@github.com>
author: GitHub Action <action@github.com>
signoff: true
branch: sync_version
title: ":floppy_disk: Update Version"
body: |
Auto-generated by [create-pull-request][1]
[1]: https://github.com/peter-evans/create-pull-request
draft: false
delete-branch: true
labels: github-actions
sync-readme:
runs-on: ubuntu-latest
steps:

View File

@@ -1,6 +1,7 @@
# New Database Backup and Import Functionality
**Full activation will take place on approximately January 5th, 2025!**
> [!IMPORTANT]
> **Full activation will take place on approximately January 5th, 2025!**
Why is the waiting time six months?

View File

@@ -7,10 +7,11 @@ Stirling-PDF is a robust, locally hosted web-based PDF manipulation tool. This g
## 2. Project Overview
Stirling-PDF is built using:
- Spring Boot + Thymeleaf
- PDFBox
- LibreOffice
- OcrMyPdf
- qpdf
- HTML, CSS, JavaScript
- Docker
- PDF.js
@@ -20,14 +21,17 @@ Stirling-PDF is built using:
## 3. Development Environment Setup
### Prerequisites
- Docker
- Git
- Java JDK 17 or later
- Gradle 7.0 or later (Included within repo)
### Setup Steps
1. Clone the repository:
```
```bash
git clone https://github.com/Stirling-Tools/Stirling-PDF.git
cd Stirling-PDF
```
@@ -43,10 +47,9 @@ Visit the [Lombok website](https://projectlombok.org/setup/) for installation in
5. Add environment variable
For local testing you should generally be testing the full 'Security' version of Stirling-PDF to do this you must add the environment flag DOCKER_ENABLE_SECURITY=true to your system and/or IDE build/run step
## 4. Project Structure
```
```bash
Stirling-PDF/
├── .github/ # GitHub-specific files (workflows, issue templates)
├── configs/ # Configuration files used by stirling at runtime (generated at runtime)
@@ -92,6 +95,7 @@ Stirling-PDF/
## 5. Docker-based Development
Stirling-PDF offers several Docker versions:
- Full: All features included
- Ultra-Lite: Basic PDF operations only
- Fat: Includes additional libraries and fonts predownloaded
@@ -110,7 +114,7 @@ These files provide pre-configured setups for different scenarios. For example,
services:
stirling-pdf:
container_name: Stirling-PDF-Security
image: frooodle/s-pdf:latest
image: stirlingtools/stirling-pdf:latest
deploy:
resources:
limits:
@@ -153,11 +157,13 @@ docker-compose -f exampleYmlFiles/docker-compose-latest-security.yml up
Stirling-PDF uses different Docker images for various configurations. The build process is controlled by environment variables and uses specific Dockerfile variants. Here's how to build the Docker images:
1. Set the security environment variable:
```bash
export DOCKER_ENABLE_SECURITY=false # or true for security-enabled builds
```
2. Build the project with Gradle:
```bash
./gradlew clean build
```
@@ -165,25 +171,26 @@ Stirling-PDF uses different Docker images for various configurations. The build
3. Build the Docker images:
For the latest version:
```bash
docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t frooodle/s-pdf:latest -f ./Dockerfile .
docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t stirlingtools/stirling-pdf:latest -f ./Dockerfile .
```
For the ultra-lite version:
```bash
docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t frooodle/s-pdf:latest-ultra-lite -f ./Dockerfile-ultra-lite .
docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t stirlingtools/stirling-pdf:latest-ultra-lite -f ./Dockerfile-ultra-lite .
```
For the fat version (with security enabled):
```bash
export DOCKER_ENABLE_SECURITY=true
docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t frooodle/s-pdf:latest-fat -f ./Dockerfile-fat .
docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t stirlingtools/stirling-pdf:latest-fat -f ./Dockerfile-fat .
```
Note: The `--no-cache` and `--pull` flags ensure that the build process uses the latest base images and doesn't use cached layers, which is useful for testing and ensuring reproducible builds. however to improve build times these can often be removed depending on your usecase
## 6. Testing
### Comprehensive Testing Script
@@ -197,6 +204,7 @@ To run the test script:
```
This script performs the following actions:
1. Builds all Docker images (full, ultra-lite, fat)
2. Runs each version to ensure it starts correctly
3. Executes Cucumber tests against main version and ensures feature compatibility, in the event these tests fail your PR will not be merged
@@ -209,7 +217,6 @@ Note: The `test.sh` script will run automatically when you raise a PR. However,
2. Access the application at `http://localhost:8080` and manually test all features developed.
### Local Testing (Java and UI Components)
For quick iterations and development of Java backend, JavaScript, and UI components, you can run and test Stirling-PDF locally without Docker. This approach allows you to work on and verify changes to:
@@ -223,7 +230,8 @@ For quick iterations and development of Java backend, JavaScript, and UI compone
To run Stirling-PDF locally:
1. Compile and run the project using built in IDE methods or by running:
```
```bash
./gradlew bootRun
```
@@ -234,11 +242,11 @@ To run Stirling-PDF locally:
4. For API changes, use tools like Postman or curl to test endpoints directly.
Important notes:
- Local testing doesn't include features that depend on external tools like OCRmyPDF, LibreOffice, or Python scripts.
- Local testing doesn't include features that depend on external tools like qpdf, LibreOffice, or Python scripts.
- There are currently no automated unit tests. All testing is done manually through the UI or API calls. (You are welcome to add JUnits!)
- Always verify your changes in the full Docker environment before submitting pull requests, as some integrations and features will only work in the complete setup.
## 7. Contributing
1. Fork the repository on GitHub.
@@ -246,14 +254,17 @@ Important notes:
3. Make your changes and commit them with clear, descriptive messages and ensure any documentation is updated related to your changes.
4. Test your changes thoroughly in the Docker environment.
5. Run the `test.sh` script to ensure all versions build correctly and pass the Cucumber tests:
```bash
./test.sh
```
6. Push your changes to your fork.
7. Submit a pull request to the main repository.
7. Submit a pull request to the main repository.
8. See additional [contributing guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
When you raise a PR:
- The `test.sh` script will run automatically against your PR.
- The PR checks will verify versioning and dependency updates.
- Documentation will be automatically updated for dependency changes.
@@ -268,6 +279,7 @@ API documentation is available at `/swagger-ui/index.html` when running the appl
## 9. Customization
Stirling-PDF can be customized through environment variables or a `settings.yml` file. Key customization options include:
- Application name and branding
- Security settings
- UI customization
@@ -276,7 +288,8 @@ Stirling-PDF can be customized through environment variables or a `settings.yml`
When using Docker, pass environment variables using the `-e` flag or in your `docker-compose.yml` file.
Example:
```
```bash
docker run -p 8080:8080 -e APP_NAME="My PDF Tool" stirling-pdf:full
```
@@ -293,16 +306,14 @@ For managing language translations that affect multiple files, Stirling-PDF prov
This script helps you make consistent replacements across language files.
When contributing translations:
1. Use the helper script for multi-file changes.
2. Ensure all language files are updated consistently.
3. The PR checks will verify consistency in language file updates.
Remember to test your changes thoroughly to ensure they don't break any existing functionality.
# Code examples
## Code examples
### Overview of Thymeleaf
@@ -311,22 +322,28 @@ Thymeleaf is a server-side Java HTML template engine. It is used in Stirling-PD
### 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 `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
Some examples of this are:
```html
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
or
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
```
Where it uses the th:block, th: indicating its a special thymeleaf element to be used serverside 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
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 java side into html side.
```java
@GetMapping
public String newFeaturePage(Model model) {
@@ -334,7 +351,9 @@ Thymeleaf can also be used to loop through objects or pass things from java side
return "new-feature";
}
```
in 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 -->
@@ -346,6 +365,7 @@ in above example if exampleData is a list of plain java objects of class Person
</tr>
</tbody>
```
This would generate n entries of tr for each person in exampleData
### Adding a New Feature to the Backend (API)
@@ -397,34 +417,35 @@ This would generate n entries of tr for each person in exampleData
```
2b. **Integrate the Service with the Controller:**
- Autowire the service class in the controller and use it to handle the API request.
```java
package stirling.software.SPDF.controller.api;
- Autowire the service class in the controller and use it to handle the API request.
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import stirling.software.SPDF.service.NewFeatureService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
```java
package stirling.software.SPDF.controller.api;
@RestController
@RequestMapping("/api/v1/new-feature")
@Tag(name = "General", description = "General APIs")
public class NewFeatureController {
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import stirling.software.SPDF.service.NewFeatureService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
@Autowired
private NewFeatureService newFeatureService;
@RestController
@RequestMapping("/api/v1/new-feature")
@Tag(name = "General", description = "General APIs")
public class NewFeatureController {
@GetMapping
@Operation(summary = "New Feature", description = "This is a new feature endpoint.")
public String newFeature() {
return newFeatureService.getNewFeatureData();
}
}
```
@Autowired
private NewFeatureService newFeatureService;
@GetMapping
@Operation(summary = "New Feature", description = "This is a new feature endpoint.")
public String newFeature() {
return newFeatureService.getNewFeatureData();
}
}
```
### Adding a New Feature to the Frontend (UI)
@@ -511,7 +532,6 @@ This would generate n entries of tr for each person in exampleData
</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:
@@ -522,13 +542,13 @@ Find the existing `messages.properties` files in the `src/main/resources` direct
- `messages.properties` (default, usually English)
- `messages_en_GB.properties`
- `messages_fr.properties`
- `messages_de.properties`
- `messages_fr_FR.properties`
- `messages_de_DE.properties`
- etc.
### 2. Add New Translation Entries
Open each of these files and add your new translation entries. For example, if you're adding a new feature called "PDF Splitter",
Open each of these files and add your new translation entries. For example, if you're adding a new feature called "PDF Splitter",
Use descriptive, hierarchical keys (e.g., `feature.element.description`)
you might add:
@@ -552,6 +572,4 @@ In your Thymeleaf templates, use the `#{key}` syntax to reference the new transl
<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.

View File

@@ -6,7 +6,6 @@ COPY scripts /scripts
COPY pipeline /pipeline
COPY src/main/resources/static/fonts/*.ttf /usr/share/fonts/opentype/noto/
#COPY src/main/resources/static/fonts/*.otf /usr/share/fonts/opentype/noto/
COPY build/libs/*.jar app.jar
ARG VERSION_TAG
@@ -19,6 +18,10 @@ ENV DOCKER_ENABLE_SECURITY=false \
PGID=1000 \
UMASK=022
# Create non-root user first
RUN addgroup -S stirlingpdfgroup && \
adduser -S stirlingpdfuser -G stirlingpdfgroup
# JDK for app
RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/apk/repositories && \
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/community" | tee -a /etc/apk/repositories && \
@@ -30,8 +33,7 @@ RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /et
tini \
bash \
curl \
shadow \
su-exec \
qpdf \
openssl \
openssl-dev \
openjdk21-jre \
@@ -40,7 +42,6 @@ RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /et
# pdftohtml
poppler-utils \
# OCR MY PDF (unpaper for descew and other advanced features)
ocrmypdf \
tesseract-ocr-data-eng \
# CV
py3-opencv \
@@ -50,15 +51,16 @@ RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /et
# uno unoconv and HTML
pip install --break-system-packages --no-cache-dir --upgrade unoconv WeasyPrint pdf2image pillow && \
mv /usr/share/tessdata /usr/share/tessdata-original && \
mkdir -p $HOME /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders && \
fc-cache -f -v && \
chmod +x /scripts/* && \
chmod +x /scripts/init.sh && \
# User permissions
addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \
chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /usr/share/fonts/opentype/noto /configs /customFiles /pipeline && \
chown stirlingpdfuser:stirlingpdfgroup /app.jar && \
tesseract --list-langs
mkdir -p ${HOME} /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders /scripts /usr/share/fonts/custom && \
chown -R stirlingpdfuser:stirlingpdfgroup ${HOME} /configs /logs /customFiles /pipeline /scripts /usr/share/fonts/custom && \
chmod -R 755 ${HOME} /configs /customFiles /pipeline /scripts /usr/share/fonts/custom && \
tesseract --list-langs && \
chmod -R 777 /logs
COPY build/libs/*.jar app.jar
RUN chown stirlingpdfuser:stirlingpdfgroup /app.jar
EXPOSE 8080/tcp

View File

@@ -1,5 +1,5 @@
# Build the application
FROM gradle:8.7-jdk17 AS build
# Build stage
FROM gradle:8.11-jdk17 AS build
# Set the working directory
WORKDIR /app
@@ -7,18 +7,20 @@ WORKDIR /app
# Copy the entire project to the working directory
COPY . .
# Build the application with DOCKER_ENABLE_SECURITY=false
# Build the application
RUN DOCKER_ENABLE_SECURITY=true \
./gradlew clean build
./gradlew clean build
# Main stage
FROM alpine:3.20.3
# Copy necessary files
# Create non-root user first
RUN addgroup -S stirlingpdfgroup && \
adduser -S stirlingpdfuser -G stirlingpdfgroup
COPY scripts /scripts
COPY pipeline /pipeline
COPY src/main/resources/static/fonts/*.ttf /usr/share/fonts/opentype/noto/
COPY --from=build /app/build/libs/*.jar app.jar
ARG VERSION_TAG
@@ -33,51 +35,45 @@ ENV DOCKER_ENABLE_SECURITY=false \
FAT_DOCKER=true \
INSTALL_BOOK_AND_ADVANCED_HTML_OPS=false
# Create necessary directories with correct permissions
RUN mkdir -p ${HOME} /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders /scripts /usr/share/fonts/custom && \
chown -R stirlingpdfuser:stirlingpdfgroup ${HOME} /configs /logs /customFiles /pipeline /scripts /usr/share/fonts/custom && \
chmod -R 755 ${HOME} /configs /logs /customFiles /pipeline /scripts /usr/share/fonts/custom
# JDK for app
RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/apk/repositories && \
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/community" | tee -a /etc/apk/repositories && \
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/testing" | tee -a /etc/apk/repositories && \
# JDK and other dependencies
RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" >> /etc/apk/repositories && \
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/community" >> /etc/apk/repositories && \
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/testing" >> /etc/apk/repositories && \
apk upgrade --no-cache -a && \
apk add --no-cache \
ca-certificates \
tzdata \
tini \
bash \
curl \
shadow \
su-exec \
openssl \
openssl-dev \
openjdk21-jre \
# Doc conversion
libreoffice \
# pdftohtml
poppler-utils \
# OCR MY PDF (unpaper for descew and other advanced featues)
ocrmypdf \
tesseract-ocr-data-eng \
font-terminus font-dejavu font-noto font-noto-cjk font-awesome font-noto-extra \
# CV
py3-opencv \
# python3/pip
python3 \
ca-certificates \
tzdata \
tini \
bash \
curl \
openssl \
openssl-dev \
openjdk21-jre \
libreoffice \
poppler-utils \
qpdf \
tesseract-ocr-data-eng \
tesseract-ocr-data-fra \
font-terminus font-dejavu font-noto font-noto-cjk font-awesome font-noto-extra \
py3-opencv \
python3 \
py3-pip && \
# uno unoconv and HTML
pip install --break-system-packages --no-cache-dir --upgrade unoconv WeasyPrint pdf2image pillow && \
mv /usr/share/tessdata /usr/share/tessdata-original && \
mkdir -p $HOME /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders && \
fc-cache -f -v && \
chmod +x /scripts/* && \
chmod +x /scripts/init.sh && \
# User permissions
addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \
chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /usr/share/fonts/opentype/noto /configs /customFiles /pipeline && \
chown stirlingpdfuser:stirlingpdfgroup /app.jar && \
tesseract --list-langs
mkdir -p /usr/share/tessdata && \
chown -R stirlingpdfuser:stirlingpdfgroup /usr/share/tessdata /usr/share/fonts/opentype/noto && \
fc-cache -f -v
COPY build/libs/*.jar app.jar
RUN chown stirlingpdfuser:stirlingpdfgroup /app.jar
EXPOSE 8080/tcp
# Set user and run command
ENTRYPOINT ["tini", "--", "/scripts/init.sh"]
CMD ["java", "-Dfile.encoding=UTF-8", "-jar", "/app.jar"]
CMD ["java", "-Dfile.encoding=UTF-8", "-jar", "/app.jar"]

View File

@@ -17,8 +17,11 @@ COPY scripts/download-security-jar.sh /scripts/download-security-jar.sh
COPY scripts/init-without-ocr.sh /scripts/init-without-ocr.sh
COPY scripts/installFonts.sh /scripts/installFonts.sh
COPY pipeline /pipeline
COPY build/libs/*.jar app.jar
# Create non-root user first
RUN addgroup -S stirlingpdfgroup && \
adduser -S stirlingpdfuser -G stirlingpdfgroup
# Set up necessary directories and permissions
RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/apk/repositories && \
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/community" | tee -a /etc/apk/repositories && \
@@ -30,18 +33,15 @@ RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /et
tini \
bash \
curl \
shadow \
su-exec \
openjdk21-jre && \
# User permissions
mkdir -p /configs /logs /customFiles /usr/share/fonts/opentype/noto && \
chmod +x /scripts/*.sh && \
addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \
chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /configs /customFiles /pipeline && \
chown stirlingpdfuser:stirlingpdfgroup /app.jar
mkdir -p ${HOME} /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders /scripts /usr/share/fonts/custom && \
chown -R stirlingpdfuser:stirlingpdfgroup ${HOME} /configs /logs /customFiles /pipeline /scripts /usr/share/fonts/custom && \
chmod -R 755 ${HOME} /configs /customFiles /pipeline /scripts /usr/share/fonts/custom && \
chmod -R 777 /logs
# Set environment variables
ENV ENDPOINTS_GROUPS_TO_REMOVE=CLI
COPY build/libs/*.jar app.jar
RUN chown stirlingpdfuser:stirlingpdfgroup /app.jar
EXPOSE 8080/tcp

View File

@@ -1,47 +1,46 @@
| Operation | PageOps | Convert | Security | Other | CLI | Python | OpenCV | LibreOffice | OCRmyPDF | Java | Javascript |
| ------------------- | ------- | ------- | -------- | ----- | --- | ------ | ------ | ----------- | -------- | ---- | ---------- |
| adjust-contrast | ✔️ | | | | | | | | | | ✔️ |
| auto-split-pdf | ✔️ | | | | | | | | | ✔️ | |
| crop | ✔️ | | | | | | | | | ✔️ | |
| extract-page | ✔️ | | | | | | | | | ✔️ | |
| merge-pdfs | ✔️ | | | | | | | | | ✔️ | |
| multi-page-layout | ✔️ | | | | | | | | | ✔️ | |
| pdf-organizer | ✔️ | | | | | | | | | ✔️ | ✔️ |
| pdf-to-single-page | ✔️ | | | | | | | | | ✔️ | |
| remove-pages | ✔️ | | | | | | | | | ✔️ | |
| rotate-pdf | ✔️ | | | | | | | | | ✔️ | |
| scale-pages | ✔️ | | | | | | | | | ✔️ | |
| split-pdfs | ✔️ | | | | | | | | | ✔️ | |
| file-to-pdf | | ✔️ | | | ✔️ | | | ✔️ | | | |
| img-to-pdf | | ✔️ | | | | | | | | ✔️ | |
| pdf-to-html | | ✔️ | | | ✔️ | | | ✔️ | | | |
| pdf-to-img | | ✔️ | | | | ✔️ | | | | ✔️ | |
| pdf-to-pdfa | | ✔️ | | | ✔️ | | | | ✔️ | | |
| pdf-to-markdown | | ✔️ | | | | | | | | ✔️ | |
| pdf-to-presentation | | ✔️ | | | ✔️ | | | ✔️ | | | |
| pdf-to-text | | ✔️ | | | ✔️ | | | ✔️ | | | |
| pdf-to-word | | ✔️ | | | ✔️ | | | ✔️ | | | |
| pdf-to-xml | | ✔️ | | | ✔️ | | | ✔️ | | | |
| xlsx-to-pdf | | ✔️ | | | ✔️ | | | ✔️ | | | |
| add-password | | | ✔️ | | | | | | | ✔️ | |
| add-watermark | | | ✔️ | | | | | | | ✔️ | |
| cert-sign | | | ✔️ | | | | | | | ✔️ | |
| remove-cert-sign | | | ✔️ | | | | | | | ✔️ | |
| change-permissions | | | ✔️ | | | | | | | ✔️ | |
| remove-password | | | ✔️ | | | | | | | ✔️ | |
| sanitize-pdf | | | ✔️ | | | | | | | ✔️ | |
| add-image | | | | ✔️ | | | | | | ✔️ | |
| add-page-numbers | | | | ✔️ | | | | | | ✔️ | |
| auto-rename | | | | ✔️ | | | | | | ✔️ | |
| change-metadata | | | | ✔️ | | | | | | ✔️ | |
| compare | | | | ✔️ | | | | | | | ✔️ |
| compress-pdf | | | | ✔️ | ✔️ | | | | ✔️ | | |
| extract-image-scans | | | | ✔️ | ✔️ | ✔️ | ✔️ | | | | |
| extract-images | | | | ✔️ | | | | | | ✔️ | |
| flatten | | | | ✔️ | | | | | | | ✔️ |
| get-info-on-pdf | | | | ✔️ | | | | | | ✔️ | |
| ocr-pdf | | | | ✔️ | ✔️ | | | | ✔️ | | |
| remove-blanks | | | | ✔️ | ✔️ | ✔️ | ✔️ | | | | |
| repair | | | | ✔️ | ✔️ | | | ✔️ | | | |
| show-javascript | | | | ✔️ | | | | | | | ✔️ |
| sign | | | | ✔️ | | | | | | | ✔️ |
| Operation | PageOps | Convert | Security | Other | CLI | Python | OpenCV | LibreOffice | qpdf | Java | Javascript | Unoconv | tesseract |
| ------------------- | ------- | ------- | -------- | ----- | --- | ------ | ------ | ----------- | -------- | ---- | ---------- | ------- | ----------- |
| adjust-contrast | ✔️ | | | | | | | | | | ✔️ | | |
| auto-split-pdf | ✔️ | | | | | | | | | ✔️ | | | |
| crop | ✔️ | | | | | | | | | ✔️ | | | |
| extract-page | ✔️ | | | | | | | | | ✔️ | | | |
| merge-pdfs | ✔️ | | | | | | | | | ✔️ | | | |
| multi-page-layout | ✔️ | | | | | | | | | ✔️ | | | |
| pdf-organizer | ✔️ | | | | | | | | | ✔️ | ✔️ | | |
| pdf-to-single-page | ✔️ | | | | | | | | | ✔️ | | | |
| remove-pages | ✔️ | | | | | | | | | ✔️ | | | |
| rotate-pdf | ✔️ | | | | | | | | | ✔️ | | | |
| scale-pages | ✔️ | | | | | | | | | ✔️ | | | |
| split-pdfs | ✔️ | | | | | | | | | ✔️ | | | |
| file-to-pdf | | ✔️ | | | ✔️ | ✔️ | | ✔️ | | | | ✔️ | |
| img-to-pdf | | ✔️ | | | | | | | | ✔️ | | | |
| pdf-to-html | | ✔️ | | | ✔️ | | | ✔️ | | | | | |
| pdf-to-img | | ✔️ | | | | ✔️ | | | | ✔️ | | | |
| pdf-to-pdfa | | ✔️ | | | ✔️ | | | | ✔️ | | | | |
| pdf-to-markdown | | ✔️ | | | | | | | | ✔️ | | | |
| pdf-to-presentation | | ✔️ | | | ✔️ | | | ✔️ | | | | | |
| pdf-to-text | | ✔️ | | | ✔️ | | | ✔️ | | | | | |
| pdf-to-word | | ✔️ | | | ✔️ | | | ✔️ | | | | | |
| pdf-to-xml | | ✔️ | | | ✔️ | | | ✔️ | | | | | |
| add-password | | | ✔️ | | | | | | | ✔️ | | | |
| add-watermark | | | ✔️ | | | | | | | ✔️ | | | |
| cert-sign | | | ✔️ | | | | | | | ✔️ | | | |
| remove-cert-sign | | | ✔️ | | | | | | | ✔️ | | | |
| change-permissions | | | ✔️ | | | | | | | ✔️ | | | |
| remove-password | | | ✔️ | | | | | | | ✔️ | | | |
| sanitize-pdf | | | ✔️ | | | | | | | ✔️ | | | |
| add-image | | | | ✔️ | | | | | | ✔️ | | | |
| add-page-numbers | | | | ✔️ | | | | | | ✔️ | | | |
| auto-rename | | | | ✔️ | | | | | | ✔️ | | | |
| change-metadata | | | | ✔️ | | | | | | ✔️ | | | |
| compare | | | | ✔️ | | | | | | | ✔️ | | |
| compress-pdf | | | | ✔️ | ✔️ | | | | ✔️ | | | | |
| extract-image-scans | | | | ✔️ | ✔️ | ✔️ | ✔️ | | | | | | |
| extract-images | | | | ✔️ | | | | | | ✔️ | | | |
| flatten | | | | ✔️ | | | | | | | ✔️ | | |
| get-info-on-pdf | | | | ✔️ | | | | | | ✔️ | | | |
| ocr-pdf | | | | ✔️ | ✔️ | | | | | | | | ✔ |
| remove-blanks | | | | ✔️ | ✔️ | ✔️ | ✔️ | | | | | | |
| repair | | | | ✔️ | ✔️ | | | ✔️ | ✔ | | | | |
| show-javascript | | | | ✔️ | | | | | | | ✔️ | | |
| sign | | | | ✔️ | | | | | | | ✔️ | | |

View File

@@ -1,33 +1,41 @@
## User Guide for Local Directory Scanning and File Processing
### Setting Up Watched Folders:
### Setting Up Watched Folders
- Create a folder where you want your files to be monitored. This is your 'watched folder'.
- The default directory for this is `./pipeline/watchedFolders/`
- Place any directories you want to be scanned into this folder, this folder should contain multiple folders each for their own tasks and pipelines.
- The default directory for this is `./pipeline/watchedFolders/`.
- Place any directories you want to be scanned into this folder. This folder should contain multiple folders, each for their own tasks and pipelines.
### Configuring Processing with JSON Files:
- In each directory you want processed (e.g `./pipeline/watchedFolders/officePrinter`), include a JSON configuration file.
- This JSON file should specify how you want the files in the directory to be handled (e.g., what operations to perform on them) which can be made, configured and downloaded from Stirling-PDF Pipeline interface.r
### Configuring Processing with JSON Files
- In each directory you want processed (e.g., `./pipeline/watchedFolders/officePrinter`), include a JSON configuration file.
- This JSON file should specify how you want the files in the directory to be handled (e.g., what operations to perform on them). This can be made, configured, and downloaded from the Stirling-PDF Pipeline interface.
### Automatic Scanning and Processing
### Automatic Scanning and Processing:
- The system automatically checks the watched folder every minute for new directories and files to process.
- When a directory with a valid JSON configuration file is found, it begins processing the files inside as per the configuration.
- When a directory with a valid JSON configuration file is found, it begins processing the files inside according to the configuration.
### Processing Steps
### Processing Steps:
- Files in each directory are processed according to the instructions in the JSON file.
- This might involve file conversions, data filtering, renaming files, etc. If the output of a step is a zip, this zip will be automatically unzipped as it passes to next process.
- This might involve file conversions, data filtering, renaming files, etc. If the output of a step is a zip, this zip will be automatically unzipped as it passes to the next process.
### Results and Output
### Results and Output:
- After processing, the results are saved in a specified output location. This could be a different folder or location as defined in the JSON file or the default location `./pipeline/finishedFolders/`.
- Each processed file is named and organized according to the rules set in the JSON configuration.
### Completion and Cleanup:
### Completion and Cleanup
- Once processing is complete, the original files in the watched folder's directory are removed.
- You can find the processed files in the designated output location.
### Error Handling:
### Error Handling
- If there's an error during processing, the system will not delete the original files, allowing you to check and retry if necessary.
### User Interaction:
### User Interaction
- As a user, your main tasks are to set up the watched folders, place directories with files for processing, and create the corresponding JSON configuration files.
- The system handles the rest, including scanning, processing, and outputting results.

View File

@@ -1,43 +1,47 @@
<p align="center"><img src="https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/main/docs/stirling.png" width="80" ><br><h1 align="center">Stirling-PDF</h1>
<p align="center">
<img src="https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/main/docs/stirling.png" width="80">
<br>
<h1 align="center">Stirling-PDF</h1>
</p>
# How to add new languages to Stirling-PDF
Fork Stirling-PDF and make a new branch out of Main
Fork Stirling-PDF and create a new branch out of `main`.
Then add reference to the language in the navbar by adding a new language entry to the dropdown
Then add a reference to the language in the navbar by adding a new language entry to the dropdown:
https://github.com/Stirling-Tools/Stirling-PDF/blob/main/src/main/resources/templates/fragments/languages.html
and add a flag svg file to
https://github.com/Stirling-Tools/Stirling-PDF/tree/main/src/main/resources/static/images/flags
Any SVG flags are fine, i got most of mine from [here](https://flagicons.lipis.dev/)
If your language isn't represented by a flag just find whichever closely matches it, such as for Arabic i chose Saudi Arabia
- Edit the file: [languages.html](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/src/main/resources/templates/fragments/languages.html)
- Add a flag SVG file to: [flags directory](https://github.com/Stirling-Tools/Stirling-PDF/tree/main/src/main/resources/static/images/flags)
For example to add Polish you would add
Any SVG flags are fine; most of the current ones were sourced from [here](https://flagicons.lipis.dev/). If your language isn't represented by a flag, choose a similar one, such as Saudi Arabia's flag for Arabic.
For example, to add Polish, you would add:
```html
<a class="dropdown-item lang_dropdown-item" href="" data-language-code="pl_PL">
<a class="dropdown-item lang_dropdown-item" href="" data-bs-language-code="pl_PL">
<img src="images/flags/pl.svg" alt="icon" width="20" height="15"> Polski
</a>
```
The data-language-code is the code used to reference the file in the next step.
The `data-bs-language-code` is the code used to reference the file in the next step.
Start by copying the existing english property file
### Add Language Property File
[https://github.com/Stirling-Tools/Stirling-PDF/blob/main/src/main/resources/messages_en_GB.properties](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/src/main/resources/messages_en_GB.properties)
Start by copying the existing English property file:
Copy and rename it to messages_{your data-language-code here}.properties, in the polish example you would set the name to messages_pl_PL.properties
- [messages_en_GB.properties](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/src/main/resources/messages_en_GB.properties)
Then simply translate all property entries within that file and make a PR into main for others to use!
Copy and rename it to `messages_{your data-bs-language-code here}.properties`. In the Polish example, you would set the name to `messages_pl_PL.properties`.
If you do not have a java IDE i am happy to verify the changes worked once you raise PR (but won't be able to verify the translations themselves)
Then simply translate all property entries within that file and make a Pull Request (PR) into `main` for others to use!
If you do not have a Java IDE, I am happy to verify that the changes work once you raise the PR (but I won't be able to verify the translations themselves).
## Handling Untranslatable Strings
Sometimes, certain strings in the properties file may not require translation because they are the same in the target language or are universal (like names of protocols, certain terminologies, etc.). To ensure accurate statistics for language progress, these strings should be added to the `ignore_translation.toml` file located in the `scripts` directory. This will exclude them from the translation progress calculations.
For example, if the English string error=Error does not need translation in Polish, add it to the ignore_translation.toml under the Polish section:
For example, if the English string `error=Error` does not need translation in Polish, add it to the `ignore_translation.toml` under the Polish section:
```toml
[pl_PL]
@@ -49,7 +53,9 @@ ignore = [
## Add New Translation Tags
- **Important**: If you add any new translation tags, they must first be added to the `messages_en_GB.properties` file. This ensures consistency across all language files.
> [!IMPORTANT]
> If you add any new translation tags, they must first be added to the `messages_en_GB.properties` file. This ensures consistency across all language files.
- New translation tags **must be added** to the `messages_en_GB.properties` file to maintain a reference for other languages.
- After adding the new tags to `messages_en_GB.properties`, add and translate them in the respective language file (e.g., `messages_pl_PL.properties`).

View File

@@ -3,34 +3,36 @@
This document provides instructions on how to add additional language packs for the OCR tab in Stirling-PDF, both inside and outside of Docker.
## My OCR used to work and now doesn't!
The paths have changed for the tessadata locations on new docker images, please use ``/usr/share/tessdata`` (Others should still work for backwards compatibility but might not)
The paths have changed for the tessdata locations on new Docker images. Please use `/usr/share/tessdata` (Others should still work for backward compatibility but might not).
## How does the OCR Work
Stirling-PDF uses [OCRmyPDF](https://github.com/ocrmypdf/OCRmyPDF) which in turn uses tesseract for its text recognition.
All credit goes to them for this awesome work!
Stirling-PDF uses Tesseract for its text recognition. All credit goes to them for this awesome work!
## Language Packs
Tesseract OCR supports a variety of languages. You can find additional language packs in the Tesseract GitHub repositories:
- [tessdata_fast](https://github.com/tesseract-ocr/tessdata_fast): These language packs are smaller and faster to load, but may provide lower recognition accuracy.
- [tessdata_fast](https://github.com/tesseract-ocr/tessdata_fast): These language packs are smaller and faster to load but may provide lower recognition accuracy.
- [tessdata](https://github.com/tesseract-ocr/tessdata): These language packs are larger and provide better recognition accuracy, but may take longer to load.
Depending on your requirements, you can choose the appropriate language pack for your use case. By default Stirling-PDF uses the tessdata_fast eng but this can be replaced.
Depending on your requirements, you can choose the appropriate language pack for your use case. By default, Stirling-PDF uses `tessdata_fast` for English, but this can be replaced.
### Installing Language Packs
1. Download the desired language pack(s) by selecting the `.traineddata` file(s) for the language(s) you need.
2. Place the `.traineddata` files in the Tesseract tessdata directory: `/usr/share/tessdata`
# DO NOT REMOVE EXISTING ENG.TRAINEDDATA, IT'S REQUIRED.
**DO NOT REMOVE EXISTING `eng.traineddata`, IT'S REQUIRED.**
#### Docker
### Docker Setup
If you are using Docker, you need to expose the Tesseract tessdata directory as a volume in order to use the additional language packs.
#### Docker Compose
Modify your `docker-compose.yml` file to include the following volume configuration:
#### Docker Compose
Modify your `docker-compose.yml` file to include the following volume configuration:
```yaml
services:
@@ -40,18 +42,17 @@ services:
- /location/of/trainingData:/usr/share/tessdata
```
#### Docker Run
Add the following to your existing Docker run command:
#### Docker run
Add the following to your existing docker run command
```bash
-v /location/of/trainingData:/usr/share/tessdata
```
#### Non-Docker
If you are not using Docker, you need to install the OCR components, including the ocrmypdf app.
You can see [OCRmyPDF install guide](https://ocrmypdf.readthedocs.io/en/latest/installation.html)
### Non-Docker Setup
Debian based systems, install languages with this command:
For Debian-based systems, install languages with this command:
```bash
sudo apt update &&\
@@ -65,7 +66,7 @@ apt search tesseract-ocr-
dpkg-query -W tesseract-ocr- | sed 's/tesseract-ocr-//g'
```
Fedora:
For Fedora:
```bash
# All languages
@@ -77,3 +78,22 @@ dnf search -C tesseract-langpack-
# View installed languages:
rpm -qa | grep tesseract-langpack | sed 's/tesseract-langpack-//g'
```
For Windows:
You must ensure tesseract is installed
Additional languages must be downloaded manually:
Download desired .traineddata files from tessdata or tessdata_fast
Place them in the tessdata folder within your Tesseract installation directory
(e.g., C:\Program Files\Tesseract-OCR\tessdata)
Verify installation:
``tesseract --list-langs``
You must then edit your ``/configs/settings.yml`` and change the system.tessdataDir to match the directory containing lang files
```
system:
tessdataDir: C:/Program Files/Tesseract-OCR/tessdata # path to the directory containing the Tessdata files. This setting is relevant for Windows systems. For Windows users, this path should be adjusted to point to the appropriate directory where the Tessdata files are stored.
```

45
Jenkinsfile vendored
View File

@@ -1,45 +0,0 @@
pipeline {
agent any
stages {
stage('Build') {
steps {
sh 'chmod 755 gradlew'
sh './gradlew build'
}
}
stage('Docker Build') {
steps {
script {
def appVersion = sh(returnStdout: true, script: './gradlew printVersion -q').trim()
def image = "frooodle/s-pdf:$appVersion"
sh "docker build -t $image ."
}
}
}
stage('Docker Push') {
steps {
script {
def appVersion = sh(returnStdout: true, script: './gradlew printVersion -q').trim()
def image = "frooodle/s-pdf:$appVersion"
withCredentials([string(credentialsId: 'docker_hub_access_token', variable: 'DOCKER_HUB_ACCESS_TOKEN')]) {
sh "docker login --username frooodle --password $DOCKER_HUB_ACCESS_TOKEN"
sh "docker push $image"
}
}
}
}
stage('Helm Push') {
steps {
script {
//TODO: Read chartVersion from Chart.yaml
def chartVersion = '1.0.0'
withCredentials([string(credentialsId: 'docker_hub_access_token', variable: 'DOCKER_HUB_ACCESS_TOKEN')]) {
sh "docker login --username frooodle --password $DOCKER_HUB_ACCESS_TOKEN"
sh "helm package chart/stirling-pdf"
sh "helm push stirling-pdf-chart-1.0.0.tgz oci://registry-1.docker.io/frooodle"
}
}
}
}
}
}

View File

@@ -1,48 +1,35 @@
To run the application without Docker/Podman, you will need to manually install all dependencies and build the necessary components.
Note that some dependencies might not be available in the standard repositories of all Linux distributions, and may require additional steps to install.
The following guide assumes you have a basic understanding of using a command line interface in your operating system.
It should work on most Linux distributions and MacOS. For Windows, you might need to use Windows Subsystem for Linux (WSL) for certain steps.
The amount of dependencies is to actually reduce overall size, ie installing LibreOffice sub components rather than full LibreOffice package.
It should work on most Linux distributions and MacOS. For Windows, you might need to use Windows Subsystem for Linux (WSL) for certain steps. The amount of dependencies is to actually reduce overall size, i.e., installing LibreOffice subcomponents rather than the full LibreOffice package.
You could theoretically use a Distrobox/Toolbox, if your Distribution has old or not all Packages. But you might just as well use the Docker Container then.
You could theoretically use a Distrobox/Toolbox if your distribution has old or not all packages. But you might just as well use the Docker container then.
### Step 1: Prerequisites
Install the following software, if not already installed:
- Java 17 or later (21 recommended)
- Gradle 7.0 or later (included within repo so not needed on server)
- Git
- Python 3.8 (with pip)
- Make
- GCC/G++
- Automake
- Autoconf
- libtool
- pkg-config
- zlib1g-dev
- libleptonica-dev
For Debian-based systems, you can use the following command:
```bash
sudo apt-get update
sudo apt-get install -y git automake autoconf libtool libleptonica-dev pkg-config zlib1g-dev make g++ openjdk-21-jdk python3 python3-pip
sudo apt-get install -y git automake autoconf libtool libleptonica-dev pkg-config zlib1g-dev make g++ openjdk-21-jdk python3 python3-pip
```
For Fedora-based systems use this command:
@@ -52,6 +39,7 @@ sudo dnf install -y git automake autoconf libtool leptonica-devel pkg-config zli
```
For non-root users with Nix Package Manager, use the following command:
```bash
nix-channel --update
nix-env -iA nixpkgs.jdk21 nixpkgs.git nixpkgs.python38 nixpkgs.gnumake nixpkgs.libgcc nixpkgs.automake nixpkgs.autoconf nixpkgs.libtool nixpkgs.pkg-config nixpkgs.zlib nixpkgs.leptonica
@@ -63,116 +51,108 @@ For Debian and Fedora, you can build it from source using the following commands
```bash
mkdir ~/.git
cd ~/.git &&\
git clone https://github.com/agl/jbig2enc.git &&\
cd jbig2enc &&\
./autogen.sh &&\
./configure &&\
make &&\
cd ~/.git && \
git clone https://github.com/agl/jbig2enc.git && \
cd jbig2enc && \
./autogen.sh && \
./configure && \
make && \
sudo make install
```
For Nix, you will face `Leptonica not detected`. Bypass this by installing it directly using the following command:
```bash
nix-env -iA nixpkgs.jbig2enc
```
### Step 3: Install Additional Software
Next we need to install LibreOffice for conversions, ocrmypdf for OCR, and opencv for pattern recognition functionality.
Next we need to install LibreOffice for conversions, qpdf for OCR, and OpenCV for pattern recognition functionality.
Install the following software:
- libreoffice-core
- libreoffice-common
- libreoffice-writer
- libreoffice-calc
- libreoffice-impress
- python3-uno
- unoconv
- pngquant
- unpaper
- ocrmypdf
- qpdf
- opencv-python-headless
For Debian-based systems, you can use the following command:
```bash
sudo apt-get install -y libreoffice-writer libreoffice-calc libreoffice-impress unpaper ocrmypdf
sudo apt-get install -y libreoffice-writer libreoffice-calc libreoffice-impress unpaper qpdf
pip3 install uno opencv-python-headless unoconv pngquant WeasyPrint --break-system-packages
```
For Fedora:
```bash
sudo dnf install -y libreoffice-writer libreoffice-calc libreoffice-impress unpaper ocrmypdf
sudo dnf install -y libreoffice-writer libreoffice-calc libreoffice-impress unpaper qpdf
pip3 install uno opencv-python-headless unoconv pngquant WeasyPrint
```
For Nix:
```bash
nix-env -iA nixpkgs.unpaper nixpkgs.libreoffice nixpkgs.ocrmypdf nixpkgs.poppler_utils
nix-env -iA nixpkgs.unpaper nixpkgs.libreoffice nixpkgs.qpdf nixpkgs.poppler_utils
pip3 install uno opencv-python-headless unoconv pngquant WeasyPrint
```
### Step 4: Clone and Build Stirling-PDF
```bash
cd ~/.git &&\
git clone https://github.com/Stirling-Tools/Stirling-PDF.git &&\
cd Stirling-PDF &&\
chmod +x ./gradlew &&\
cd ~/.git && \
git clone https://github.com/Stirling-Tools/Stirling-PDF.git && \
cd Stirling-PDF && \
chmod +x ./gradlew && \
./gradlew build
```
### Step 5: Move jar to desired location
### Step 5: Move Jar to Desired Location
After the build process, a `.jar` file will be generated in the `build/libs` directory.
You can move this file to a desired location, for example, `/opt/Stirling-PDF/`.
You must also move the Script folder within the Stirling-PDF repo that you have downloaded to this directory.
This folder is required for the python scripts using OpenCV.
After the build process, a `.jar` file will be generated in the `build/libs` directory. You can move this file to a desired location, for example, `/opt/Stirling-PDF/`. You must also move the Script folder within the Stirling-PDF repo that you have downloaded to this directory. This folder is required for the Python scripts using OpenCV.
```bash
sudo mkdir /opt/Stirling-PDF &&\
sudo mv ./build/libs/Stirling-PDF-*.jar /opt/Stirling-PDF/ &&\
sudo mv scripts /opt/Stirling-PDF/ &&\
sudo mkdir /opt/Stirling-PDF && \
sudo mv ./build/libs/Stirling-PDF-*.jar /opt/Stirling-PDF/ && \
sudo mv scripts /opt/Stirling-PDF/ && \
echo "Scripts installed."
```
For non-root users, you can just keep the jar in the main directory of Stirling-PDF using the following command:
```bash
mv ./build/libs/Stirling-PDF-*.jar ./Stirling-PDF-*.jar
```
### Step 6: Other files
### Step 6: Other Files
#### OCR
If you plan to use the OCR (Optical Character Recognition) functionality, you might need to install language packs for Tesseract if running non-english scanning.
If you plan to use the OCR (Optical Character Recognition) functionality, you might need to install language packs for Tesseract if running non-English scanning.
##### Installing Language Packs
Easiest is to use the langpacks provided by your repositories. Skip the other steps.
Manual:
The easiest method is to use the language packs provided by your repositories. Skip the other steps if they are available.
**Manual:**
1. Download the desired language pack(s) by selecting the `.traineddata` file(s) for the language(s) you need.
2. Place the `.traineddata` files in the Tesseract tessdata directory: `/usr/share/tessdata`
3. Please view [OCRmyPDF install guide](https://ocrmypdf.readthedocs.io/en/latest/installation.html) for more info.
**IMPORTANT:** DO NOT REMOVE EXISTING `eng.traineddata`, IT'S REQUIRED.
Debian based systems, install languages with this command:
**Debian-based systems**, install languages with this command:
```bash
sudo apt update &&\
sudo apt update && \
# All languages
# sudo apt install -y 'tesseract-ocr-*'
@@ -183,7 +163,7 @@ apt search tesseract-ocr-
dpkg-query -W tesseract-ocr- | sed 's/tesseract-ocr-//g'
```
Fedora:
**Fedora:**
```bash
# All languages
@@ -196,13 +176,13 @@ dnf search -C tesseract-langpack-
rpm -qa | grep tesseract-langpack | sed 's/tesseract-langpack-//g'
```
Nix:
**Nix:**
```bash
nix-env -iA nixpkgs.tesseract
```
**Note:** Nix Package Manager pre-installs almost all the language packs when tesseract is installed.
**Note:** Nix Package Manager pre-installs almost all the language packs when Tesseract is installed.
### Step 7: Run Stirling-PDF
@@ -214,11 +194,13 @@ or
java -jar /opt/Stirling-PDF/Stirling-PDF-*.jar
```
Since libreoffice, soffice, and conversion tools have their dbus_tmp_dir set as `dbus_tmp_dir="/run/user/$(id -u)/libreoffice-dbus"`, you might get the following error when using their endpoints:
Since LibreOffice, soffice, and conversion tools have their dbus_tmp_dir set as `dbus_tmp_dir="/run/user/$(id -u)/libreoffice-dbus"`, you might get the following error when using their endpoints:
```
[Thread-7] INFO s.s.SPDF.utils.ProcessExecutor - mkdir: cannot create directory /run/user/1501: Permission denied
```
To resolve this, before starting the Stirling-PDF, you have to set the environment variable to a directory you have write access to by using the following commands:
To resolve this, before starting Stirling-PDF, you have to set the environment variable to a directory you have write access to by using the following commands:
```bash
mkdir temp
@@ -228,9 +210,10 @@ or
java -jar ./Stirling-PDF-*.jar
```
### Step 8: Adding a Desktop icon
### Step 8: Adding a Desktop Icon
This will add a modified app starter to your app menu.
This will add a modified Appstarter to your Appmenu.
```bash
location=$(pwd)/gradlew
image=$(pwd)/docs/stirling-transparent.svg
@@ -251,35 +234,40 @@ EOF
Note: Currently the app will run in the background until manually closed.
### Optional: Changing the host and port of the application:
### Optional: Changing the Host and Port of the Application
To override the default configuration, you can add the following to `/.git/Stirling-PDF/configs/custom_settings.yml` file:
```bash
```yaml
server:
host: 0.0.0.0 # Not working - use instead address
address: 0.0.0.0
port: 3000
```
'-Djava.net.preferIPv4Stack=true' --> To force ipv4 only in the java starting command
`-Djava.net.preferIPv4Stack=true` --> To force IPv4 only in the Java starting command
**Note:** This file is created after the first application launch. To have it before that, you can create the directory and add the file yourself.
### Optional: Run Stirling-PDF as a service (requires root).
### Optional: Run Stirling-PDF as a Service (requires root)
First create a .env file, where you can store environment variables:
```
First create a `.env` file, where you can store environment variables:
```bash
touch /opt/Stirling-PDF/.env
```
In this file you can add all variables, one variable per line, as stated in the main readme (for example SYSTEM_DEFAULTLOCALE="de-DE").
Create a new file where we store our service settings and open it with nano editor:
```
In this file, you can add all variables, one variable per line, as stated in the main readme (for example `SYSTEM_DEFAULTLOCALE="de-DE"`).
Create a new file where we store our service settings and open it with the nano editor:
```bash
nano /etc/systemd/system/stirlingpdf.service
```
Paste this content, make sure to update the filename of the jar-file. Press Ctrl+S and Ctrl+X to save and exit the nano editor:
```
Paste this content, make sure to update the filename of the jar file. Press `Ctrl+S` and `Ctrl+X` to save and exit the nano editor:
```ini
[Unit]
Description=Stirling-PDF service
After=syslog.target network.target
@@ -303,22 +291,25 @@ WantedBy=multi-user.target
Notify systemd that it has to rebuild its internal service database (you have to run this command every time you make a change in the service file):
```
```bash
sudo systemctl daemon-reload
```
Enable the service to tell the service to start it automatically:
```
Enable the service to tell it to start automatically:
```bash
sudo systemctl enable stirlingpdf.service
```
See the status of the service:
```
```bash
sudo systemctl status stirlingpdf.service
```
Manually start/stop/restart the service:
```
```bash
sudo systemctl start stirlingpdf.service
sudo systemctl stop stirlingpdf.service
sudo systemctl restart stirlingpdf.service
@@ -326,12 +317,11 @@ sudo systemctl restart stirlingpdf.service
---
Remember to set the necessary environment variables before running the project if you want to customize the application the list can be seen in the main readme.
Remember to set the necessary environment variables before running the project if you want to customize the application. The list can be seen in the main readme.
You can do this in the terminal by using the `export` command or -D argument to java -jar command:
You can do this in the terminal by using the `export` command or `-D` argument to the Java `-jar` command:
```bash
export APP_HOME_NAME="Stirling PDF"
or
-DAPP_HOME_NAME="Stirling PDF"
```

View File

@@ -1,6 +1,7 @@
# Pipeline Configuration and Usage Tutorial
- Configure the pipeline config file and input files to run files against it
- For reuse, download the config file and re-upload it when needed, or place it in /pipeline/defaultWebUIConfigs/ to auto-load in the web UI for all users
- Configure the pipeline config file and input files to run files against it.
- For reuse, download the config file and re-upload it when needed, or place it in `/pipeline/defaultWebUIConfigs/` to auto-load in the web UI for all users.
## Steps to Configure and Use Your Pipeline
@@ -26,19 +27,16 @@
- Use the **Validation** button to check your pipeline. A green indicator signifies correct setup; a pop-out error indicates issues.
8. **Download Pipeline Configuration**
- To use the configuration for folder scanning (or save it for future use and reupload it), you can also download a JSON file in this menu. You can also pre-load this for future use by placing it in ``/pipeline/defaultWebUIConfigs/``. It will then appear in the dropdown menu for all users to use.
- To use the configuration for folder scanning (or save it for future use and re-upload it), download a JSON file in this menu. You can also pre-load it for future use by placing it in `/pipeline/defaultWebUIConfigs/`. It will then appear in the dropdown menu for all users to use.
9. **Submit Files for Processing**
- If your pipeline is correctly set up close the configure menu, input the files and hit **Submit**.
- If your pipeline is correctly set up, close the configure menu, input the files, and hit **Submit**.
10. **Note on Web UI Limitations**
- The current web UI version does not support operations that require multiple different types of inputs, such as adding a separate image to a PDF.
### Current Limitations
- Cannot have more than one of the same operation
- Cannot input additional files via UI
- All files and operations run in serial mode
- Cannot have more than one of the same operation.
- Cannot input additional files via UI.
- All files and operations run in serial mode.

457
README.md
View File

@@ -1,4 +1,4 @@
<p align="center"><img src="https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/main/docs/stirling.png" width="80" ></p>
<p align="center"><img src="https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/main/docs/stirling.png" width="80"></p>
<h1 align="center">Stirling-PDF</h1>
[![Docker Pulls](https://img.shields.io/docker/pulls/frooodle/s-pdf)](https://hub.docker.com/r/frooodle/s-pdf)
@@ -6,12 +6,12 @@
[![Docker Image Version (tag latest semver)](https://img.shields.io/docker/v/frooodle/s-pdf/latest)](https://github.com/Stirling-Tools/Stirling-PDF/)
[![GitHub Repo stars](https://img.shields.io/github/stars/stirling-tools/stirling-pdf?style=social)](https://github.com/Stirling-Tools/stirling-pdf)
<a href="https://www.producthunt.com/posts/stirling-pdf?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-stirling&#0045;pdf" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=641239&theme=light" alt="Stirling&#0032;PDF - Open&#0032;source&#0032;locally&#0032;hosted&#0032;web&#0032;PDF&#0032;editor | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
[![Deploy to DO](https://www.deploytodo.com/do-btn-blue.svg)](https://cloud.digitalocean.com/apps/new?repo=https://github.com/Stirling-Tools/Stirling-PDF/tree/digitalOcean&refcode=c3210994b1af)
[<img src="https://www.ssdnodes.com/wp-content/uploads/2023/11/footer-logo.svg" alt="Name" height="40">](https://www.ssdnodes.com/manage/aff.php?aff=2216&register=true)
This is a robust, locally hosted web-based PDF manipulation tool using Docker. It enables you to carry out various operations on PDF files, including splitting, merging, converting, reorganizing, adding images, rotating, compressing, and more. This locally hosted web application has evolved to encompass a comprehensive set of features, addressing all your PDF requirements.
[Stirling-PDF](https://www.stirlingpdf.com) is a robust, locally hosted web-based PDF manipulation tool using Docker. It enables you to carry out various operations on PDF files, including splitting, merging, converting, reorganizing, adding images, rotating, compressing, and more. This locally hosted web application has evolved to encompass a comprehensive set of features, addressing all your PDF requirements.
Stirling PDF does not initiate any outbound calls for record-keeping or tracking purposes.
Stirling-PDF does not initiate any outbound calls for record-keeping or tracking purposes.
All files and PDFs exist either exclusively on the client side, reside in server memory only during task execution, or temporarily reside in a file solely for the execution of the task. Any file downloaded by the user will have been deleted from the server by that point.
@@ -19,7 +19,8 @@ All files and PDFs exist either exclusively on the client side, reside in server
## Features
- Dark mode support.
- Enterprise features like SSO Check [here](https://docs.stirlingpdf.com/Enterprise%20Edition)
- Dark mode support
- Custom download options
- Parallel file processing and downloads
- Custom 'Pipelines' to run multiple features in a queue
@@ -27,99 +28,108 @@ All files and PDFs exist either exclusively on the client side, reside in server
- Optional Login and Authentication support (see [here](https://github.com/Stirling-Tools/Stirling-PDF/tree/main#login-authentication) for documentation)
- Database Backup and Import (see [here](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DATABASE.md) for documentation)
## **PDF Features**
### **Page Operations**
## PDF Features
- View and modify PDFs - View multi page PDFs with custom viewing sorting and searching. Plus on page edit features like annotate, draw and adding text and images. (Using PDF.js with Joxit and Liberation.Liberation fonts)
- Full interactive GUI for merging/splitting/rotating/moving PDFs and their pages.
- Merge multiple PDFs together into a single resultant file.
- Split PDFs into multiple files at specified page numbers or extract all pages as individual files.
- Reorganize PDF pages into different orders.
- Rotate PDFs in 90-degree increments.
- Remove pages.
- Multi-page layout (Format PDFs into a multi-paged page).
- Scale page contents size by set %.
- Adjust Contrast.
- Crop PDF.
- Auto Split PDF (With physically scanned page dividers).
- Extract page(s).
- Convert PDF to a single page.
- Overlay PDFs ontop of each other
### Page Operations
### **Conversion Operations**
- View and modify PDFs - View multi-page PDFs with custom viewing, sorting, and searching. Plus on-page edit features like annotate, draw, and adding text and images. (Using PDF.js with Joxit and Liberation fonts)
- Full interactive GUI for merging/splitting/rotating/moving PDFs and their pages
- Merge multiple PDFs into a single resultant file
- Split PDFs into multiple files at specified page numbers or extract all pages as individual files
- Reorganize PDF pages into different orders
- Rotate PDFs in 90-degree increments
- Remove pages
- Multi-page layout (format PDFs into a multi-paged page)
- Scale page contents size by set percentage
- Adjust contrast
- Crop PDF
- Auto split PDF (with physically scanned page dividers)
- Extract page(s)
- Convert PDF to a single page
- Overlay PDFs on top of each other
- PDF to single page
- Split PDF by sections
- Convert PDFs to and from images.
- Convert any common file to PDF (using LibreOffice).
- Convert PDF to Word/Powerpoint/Others (using LibreOffice).
- Convert HTML to PDF.
- URL to PDF.
- Markdown to PDF.
### Conversion Operations
### **Security & Permissions**
- Convert PDFs to and from images
- Convert any common file to PDF (using LibreOffice)
- Convert PDF to Word/PowerPoint/others (using LibreOffice)
- Convert HTML to PDF
- Convert PDF to xml
- Convert PDF to CSV
- URL to PDF
- Markdown to PDF
- Add and remove passwords.
- Change/set PDF Permissions.
- Add watermark(s).
- Certify/sign PDFs.
- Sanitize PDFs.
- Auto-redact text.
### Security & Permissions
### **Other Operations**
- Add and remove passwords
- Change/set PDF permissions
- Add watermark(s)
- Certify/sign PDFs
- Sanitize PDFs
- Auto-redact text
- Add/Generate/Write signatures.
- Repair PDFs.
- Detect and remove blank pages.
- Compare 2 PDFs and show differences in text.
- Add images to PDFs.
- Compress PDFs to decrease their filesize (Using OCRMyPDF).
- Extract images from PDF.
- Extract images from Scans.
- Add page numbers.
- Auto rename file by detecting PDF header text.
- OCR on PDF (Using OCRMyPDF).
- PDF/A conversion (Using OCRMyPDF).
- Edit metadata.
- Flatten PDFs.
- Get all information on a PDF to view or export as JSON.
- Show/Detect embedded Javascript
### Other Operations
For a overview of the tasks and the technology each uses please view [Endpoint-groups.md](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/Endpoint-groups.md)
- Add/generate/write signatures
- Split by Size or PDF
- Repair PDFs
- Detect and remove blank pages
- Compare two PDFs and show differences in text
- Add images to PDFs
- Compress PDFs to decrease their filesize (using qpdf)
- Extract images from PDF
- Remove images from PDF
- Extract images from scans
- Remove annotations
- Add page numbers
- Auto rename file by detecting PDF header text
- OCR on PDF (using tesseract)
- PDF/A conversion (using libreoffice)
- Edit metadata
- Flatten PDFs
- Get all information on a PDF to view or export as JSON
- Show/detect embedded JavaScript
Demo of the app is available [here](https://stirlingpdf.io).
For an overview of the tasks and the technology each uses, please view [Endpoint-groups.md](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/Endpoint-groups.md).
## Technologies used
A demo of the app is available [here](https://stirlingpdf.io).
## Technologies Used
- Spring Boot + Thymeleaf
- [PDFBox](https://github.com/apache/pdfbox/tree/trunk)
- [LibreOffice](https://www.libreoffice.org/discover/libreoffice/) for advanced conversions
- [OcrMyPdf](https://github.com/ocrmypdf/OCRmyPDF)
- [qpdf](https://github.com/qpdf/qpdf)
- HTML, CSS, JavaScript
- Docker
- [PDF.js](https://github.com/mozilla/pdf.js)
- [PDF-LIB.js](https://github.com/Hopding/pdf-lib)
## How to use
## How to Use
### Windows
For windows users download the latest Stirling-PDF.exe from our [release](https://github.com/Stirling-Tools/Stirling-PDF/releases) section or by clicking [here](https://github.com/Stirling-Tools/Stirling-PDF/releases/latest/download/Stirling-PDF.exe)
For Windows users, download the latest Stirling-PDF.exe from our [release](https://github.com/Stirling-Tools/Stirling-PDF/releases) section or by clicking [here](https://github.com/Stirling-Tools/Stirling-PDF/releases/latest/download/Stirling-PDF.exe).
### Locally
Please view https://github.com/Stirling-Tools/Stirling-PDF/blob/main/LocalRunGuide.md
Please view the [LocalRunGuide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/LocalRunGuide.md).
### Docker / Podman
https://hub.docker.com/r/frooodle/s-pdf
> [!NOTE]
> <https://hub.docker.com/r/stirlingtools/stirling-pdf>
Stirling PDF has 3 different versions, a Full version and ultra-Lite version as well as a 'Fat' version. Depending on the types of features you use you may want a smaller image to save on space.
To see what the different versions offer please look at our [version mapping](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/Version-groups.md)
For people that don't mind about space optimization just use the latest tag.
![Docker Image Size (tag)](https://img.shields.io/docker/image-size/frooodle/s-pdf/latest?label=Stirling-PDF%20Full)
![Docker Image Size (tag)](https://img.shields.io/docker/image-size/frooodle/s-pdf/latest-ultra-lite?label=Stirling-PDF%20Ultra-Lite)
![Docker Image Size (tag)](https://img.shields.io/docker/image-size/frooodle/s-pdf/latest-fat?label=Stirling-PDF%20Fat)
Stirling-PDF has three different versions: a full version, an ultra-lite version, and a 'fat' version. Depending on the types of features you use, you may want a smaller image to save on space. To see what the different versions offer, please look at our [version mapping](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/Version-groups.md). For people that don't mind space optimization, just use the latest tag.
Please note in below examples you may need to change the volume paths as needed, current examples install them to the current working directory
eg ``./extraConfigs:/configs`` to ``/opt/stirlingpdf/extraConfigs:/configs``
![Docker Image Size (tag)](https://img.shields.io/docker/image-size/stirlingtools/stirling-pdf/latest?label=Stirling-PDF%20Full)
![Docker Image Size (tag)](https://img.shields.io/docker/image-size/stirlingtools/stirling-pdf/latest-ultra-lite?label=Stirling-PDF%20Ultra-Lite)
![Docker Image Size (tag)](https://img.shields.io/docker/image-size/stirlingtools/stirling-pdf/latest-fat?label=Stirling-PDF%20Fat)
Please note in the examples below, you may need to change the volume paths as needed, e.g., `./extraConfigs:/configs` to `/opt/stirlingpdf/extraConfigs:/configs`.
### Docker Run
@@ -129,15 +139,13 @@ docker run -d \
-v ./trainingData:/usr/share/tessdata \
-v ./extraConfigs:/configs \
-v ./logs:/logs \
# Optional customization (not required)
# -v /location/of/customFiles:/customFiles \
-e DOCKER_ENABLE_SECURITY=false \
-e INSTALL_BOOK_AND_ADVANCED_HTML_OPS=false \
-e LANGS=en_GB \
--name stirling-pdf \
frooodle/s-pdf:latest
Can also add these for customisation but are not required
-v /location/of/customFiles:/customFiles \
stirlingtools/stirling-pdf:latest
```
### Docker Compose
@@ -146,11 +154,11 @@ docker run -d \
version: '3.3'
services:
stirling-pdf:
image: frooodle/s-pdf:latest
image: stirlingtools/stirling-pdf:latest
ports:
- '8080:8080'
volumes:
- ./trainingData:/usr/share/tessdata #Required for extra OCR languages
- ./trainingData:/usr/share/tessdata # Required for extra OCR languages
- ./extraConfigs:/configs
# - ./customFiles:/customFiles/
# - ./logs:/logs/
@@ -162,203 +170,258 @@ services:
Note: Podman is CLI-compatible with Docker, so simply replace "docker" with "podman".
## Enable OCR/Compression feature
### Kubernetes
Please view https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToUseOCR.md
See the kubernetes helm chart [here](https://github.com/Stirling-Tools/Stirling-PDF-chart)
## Reuse stored files
## Enable OCR/Compression Feature
Certain functionality like ``Sign`` Supports pre-saved files stored at ``/customFiles/signatures/``, image files placed within here will be accesable to be used via webUI
Currently this supports two folder types
- ``/customFiles/signatures/ALL_USERS`` accessible to all users, useful for orginasations were many users use same files or for users not using authentication
- ``/customFiles/signatures/{username}`` such as ``/customFiles/signatures/froodle`` accessible to only the ``froodle`` username, private for all others
Please view the [HowToUseOCR.md](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToUseOCR.md).
## Reuse Stored Files
Certain functionality like `Sign` supports pre-saved files stored at `/customFiles/signatures/`. Image files placed within here will be accessible to be used via the web UI. Currently, this supports two folder types:
- `/customFiles/signatures/ALL_USERS`: Accessible to all users, useful for organizations where many users use the same files or for users not using authentication
- `/customFiles/signatures/{username}`: Such as `/customFiles/signatures/froodle`, accessible only to the `froodle` username, private for all others
## Supported Languages
Stirling PDF currently supports 38!
Stirling-PDF currently supports 37 languages!
| Language | Progress |
| ------------------------------------------- | -------------------------------------- |
| Arabic (العربية) (ar_AR) | ![93%](https://geps.dev/progress/93) |
| Basque (Euskara) (eu_ES) | ![56%](https://geps.dev/progress/56) |
| Bulgarian (Български) (bg_BG) | ![98%](https://geps.dev/progress/98) |
| Catalan (Català) (ca_CA) | ![44%](https://geps.dev/progress/44) |
| Croatian (Hrvatski) (hr_HR) | ![86%](https://geps.dev/progress/86) |
| Czech (Česky) (cs_CZ) | ![82%](https://geps.dev/progress/82) |
| Danish (Dansk) (da_DK) | ![90%](https://geps.dev/progress/90) |
| Dutch (Nederlands) (nl_NL) | ![87%](https://geps.dev/progress/87) |
| English (English) (en_GB) | ![100%](https://geps.dev/progress/100) |
| English (US) (en_US) | ![100%](https://geps.dev/progress/100) |
| French (Français) (fr_FR) | ![94%](https://geps.dev/progress/94) |
| German (Deutsch) (de_DE) | ![97%](https://geps.dev/progress/97) |
| Greek (Ελληνικά) (el_GR) | ![75%](https://geps.dev/progress/75) |
| Hindi (हिंदी) (hi_IN) | ![71%](https://geps.dev/progress/71) |
| Hungarian (Magyar) (hu_HU) | ![69%](https://geps.dev/progress/69) |
| Indonesia (Bahasa Indonesia) (id_ID) | ![95%](https://geps.dev/progress/95) |
| Irish (Gaeilge) (ga_IE) | ![89%](https://geps.dev/progress/89) |
| Italian (Italiano) (it_IT) | ![98%](https://geps.dev/progress/98) |
| Japanese (日本語) (ja_JP) | ![86%](https://geps.dev/progress/86) |
| Korean (한국어) (ko_KR) | ![76%](https://geps.dev/progress/76) |
| Norwegian (Norsk) (no_NB) | ![89%](https://geps.dev/progress/89) |
| Polish (Polski) (pl_PL) | ![98%](https://geps.dev/progress/98) |
| Portuguese (Português) (pt_PT) | ![71%](https://geps.dev/progress/71) |
| Portuguese Brazilian (Português) (pt_BR) | ![98%](https://geps.dev/progress/98) |
| Romanian (Română) (ro_RO) | ![91%](https://geps.dev/progress/91) |
| Russian (Русский) (ru_RU) | ![76%](https://geps.dev/progress/76) |
| Serbian Latin alphabet (Srpski) (sr_LATN_RS) | ![71%](https://geps.dev/progress/71) |
| Simplified Chinese (简体中文) (zh_CN) | ![92%](https://geps.dev/progress/92) |
| Slovakian (Slovensky) (sk_SK) | ![83%](https://geps.dev/progress/83) |
| Spanish (Español) (es_ES) | ![97%](https://geps.dev/progress/97) |
| Swedish (Svenska) (sv_SE) | ![93%](https://geps.dev/progress/93) |
| Thai (ไทย) (th_TH) | ![90%](https://geps.dev/progress/90) |
| Traditional Chinese (繁體中文) (zh_TW) | ![98%](https://geps.dev/progress/98) |
| Turkish (Türkçe) (tr_TR) | ![93%](https://geps.dev/progress/93) |
| Ukrainian (Українська) (uk_UA) | ![81%](https://geps.dev/progress/81) |
| Vietnamese (Tiếng Việt) (vi_VN) | ![90%](https://geps.dev/progress/90) |
| Language | Progress |
| -------------------------------------------- | -------------------------------------- |
| Arabic (العربية) (ar_AR) | ![95%](https://geps.dev/progress/95) |
| Azerbaijani (Azərbaycan Dili) (az_AZ) | ![93%](https://geps.dev/progress/93) |
| Basque (Euskara) (eu_ES) | ![52%](https://geps.dev/progress/52) |
| Bulgarian (Български) (bg_BG) | ![90%](https://geps.dev/progress/90) |
| Catalan (Català) (ca_CA) | ![84%](https://geps.dev/progress/84) |
| Croatian (Hrvatski) (hr_HR) | ![92%](https://geps.dev/progress/92) |
| Czech (Česky) (cs_CZ) | ![91%](https://geps.dev/progress/91) |
| Danish (Dansk) (da_DK) | ![90%](https://geps.dev/progress/90) |
| Dutch (Nederlands) (nl_NL) | ![90%](https://geps.dev/progress/90) |
| English (English) (en_GB) | ![100%](https://geps.dev/progress/100) |
| English (US) (en_US) | ![100%](https://geps.dev/progress/100) |
| French (Français) (fr_FR) | ![93%](https://geps.dev/progress/93) |
| German (Deutsch) (de_DE) | ![100%](https://geps.dev/progress/100) |
| Greek (Ελληνικά) (el_GR) | ![91%](https://geps.dev/progress/91) |
| Hindi (हिंदी) (hi_IN) | ![89%](https://geps.dev/progress/89) |
| Hungarian (Magyar) (hu_HU) | ![92%](https://geps.dev/progress/92) |
| Indonesian (Bahasa Indonesia) (id_ID) | ![91%](https://geps.dev/progress/91) |
| Irish (Gaeilge) (ga_IE) | ![82%](https://geps.dev/progress/82) |
| Italian (Italiano) (it_IT) | ![99%](https://geps.dev/progress/99) |
| Japanese (日本語) (ja_JP) | ![80%](https://geps.dev/progress/80) |
| Korean (한국어) (ko_KR) | ![89%](https://geps.dev/progress/89) |
| Norwegian (Norsk) (no_NB) | ![82%](https://geps.dev/progress/82) |
| Polish (Polski) (pl_PL) | ![91%](https://geps.dev/progress/91) |
| Portuguese (Português) (pt_PT) | ![91%](https://geps.dev/progress/91) |
| Portuguese Brazilian (Português) (pt_BR) | ![92%](https://geps.dev/progress/92) |
| Romanian (Română) (ro_RO) | ![84%](https://geps.dev/progress/84) |
| Russian (Русский) (ru_RU) | ![91%](https://geps.dev/progress/91) |
| Serbian Latin alphabet (Srpski) (sr_LATN_RS) | ![66%](https://geps.dev/progress/66) |
| Simplified Chinese (简体中文) (zh_CN) | ![85%](https://geps.dev/progress/85) |
| Slovakian (Slovensky) (sk_SK) | ![77%](https://geps.dev/progress/77) |
| Spanish (Español) (es_ES) | ![92%](https://geps.dev/progress/92) |
| Swedish (Svenska) (sv_SE) | ![91%](https://geps.dev/progress/91) |
| Thai (ไทย) (th_TH) | ![90%](https://geps.dev/progress/90) |
| Traditional Chinese (繁體中文) (zh_TW) | ![92%](https://geps.dev/progress/92) |
| Turkish (Türkçe) (tr_TR) | ![86%](https://geps.dev/progress/86) |
| Ukrainian (Українська) (uk_UA) | ![75%](https://geps.dev/progress/75) |
| Vietnamese (Tiếng Việt) (vi_VN) | ![83%](https://geps.dev/progress/83) |
## Contributing (creating issues, translations, fixing bugs, etc.)
## Contributing (Creating Issues, Translations, Fixing Bugs, etc.)
Please see our [Contributing Guide](CONTRIBUTING.md)!
Please see our [Contributing Guide](CONTRIBUTING.md).
## Customisation
## Stirling PDF Enterprise
Stirling PDF allows easy customization of the app.
Includes things like
Stirling PDF offers a Enterprise edition of its software, This is the same great software but with added features and comforts
### Whats included
- Prioritised Support tickets via support@stirlingpdf.com to reach directly to Stirling-PDF team for support and 1:1 meetings where applicable (Provided they come from same email domain registered with us)
- Prioritised Enhancements to Stirling-PDF where applicable
- Base SSO support
- Advanced SSO such as automated login handling (Coming very soon)
- SAML SSO (Coming very soon)
- Custom automated metadata handling
- Advanced user configurations (Coming soon)
- Plus other exciting features to come
Check out of [docs](https://docs.stirlingpdf.com/Enterprise%20Edition) on it or our official [website](https://www.stirlingpdf.com)
## Customization
Stirling-PDF allows easy customization of the app, including things like:
- Custom application name
- Custom slogans, icons, HTML, images CSS etc (via file overrides)
- Custom slogans, icons, HTML, images, CSS, etc. (via file overrides)
There are two options for this, either using the generated settings file ``settings.yml``
This file is located in the ``/configs`` directory and follows standard YAML formatting
There are two options for this, either using the generated settings file `settings.yml`, which is located in the `/configs` directory and follows standard YAML formatting, or using environment variables, which would override the settings file.
Environment variables are also supported and would override the settings file
For example in the settings.yml you have
For example, in `settings.yml`, you might have:
```yaml
security:
enableLogin: 'true'
```
To have this via an environment variable you would have ``SECURITY_ENABLELOGIN``
To have this via an environment variable, you would use `SECURITY_ENABLELOGIN`.
The Current list of settings is
The current list of settings is:
```yaml
security:
enableLogin: false # set to 'true' to enable login
csrfDisabled: true # Set to 'true' to disable CSRF protection (not recommended for production)
csrfDisabled: true # set to 'true' to disable CSRF protection (not recommended for production)
loginAttemptCount: 5 # lock user account after 5 tries; when using e.g. Fail2Ban you can deactivate the function with -1
loginResetTimeMinutes: 120 # lock account for 2 hours after x attempts
loginMethod: all # 'all' (Login Username/Password and OAuth2[must be enabled and configured]), 'normal'(only Login with Username/Password) or 'oauth2'(only Login with OAuth2)
initialLogin:
username: '' # Initial username for the first login
password: '' # Initial password for the first login
username: '' # initial username for the first login
password: '' # initial password for the first login
oauth2:
enabled: false # set to 'true' to enable login (Note: enableLogin must also be 'true' for this to work)
client:
keycloak:
issuer: '' # URL of the Keycloak realm's OpenID Connect Discovery endpoint
clientId: '' # Client ID for Keycloak OAuth2
clientSecret: '' # Client Secret for Keycloak OAuth2
scopes: openid, profile, email # Scopes for Keycloak OAuth2
useAsUsername: preferred_username # Field to use as the username for Keycloak OAuth2
clientId: '' # client ID for Keycloak OAuth2
clientSecret: '' # client secret for Keycloak OAuth2
scopes: openid, profile, email # scopes for Keycloak OAuth2
useAsUsername: preferred_username # field to use as the username for Keycloak OAuth2
google:
clientId: '' # Client ID for Google OAuth2
clientSecret: '' # Client Secret for Google OAuth2
scopes: https://www.googleapis.com/auth/userinfo.email, https://www.googleapis.com/auth/userinfo.profile # Scopes for Google OAuth2
useAsUsername: email # Field to use as the username for Google OAuth2
clientId: '' # client ID for Google OAuth2
clientSecret: '' # client secret for Google OAuth2
scopes: https://www.googleapis.com/auth/userinfo.email, https://www.googleapis.com/auth/userinfo.profile # scopes for Google OAuth2
useAsUsername: email # field to use as the username for Google OAuth2
github:
clientId: '' # Client ID for GitHub OAuth2
clientSecret: '' # Client Secret for GitHub OAuth2
scopes: read:user # Scope for GitHub OAuth2
useAsUsername: login # Field to use as the username for GitHub OAuth2
issuer: '' # set to any provider that supports OpenID Connect Discovery (/.well-known/openid-configuration) end-point
clientId: '' # Client ID from your provider
clientSecret: '' # Client Secret from your provider
clientId: '' # client ID for GitHub OAuth2
clientSecret: '' # client secret for GitHub OAuth2
scopes: read:user # scope for GitHub OAuth2
useAsUsername: login # field to use as the username for GitHub OAuth2
issuer: '' # set to any provider that supports OpenID Connect Discovery (/.well-known/openid-configuration) endpoint
clientId: '' # client ID from your provider
clientSecret: '' # client secret from your provider
autoCreateUser: false # set to 'true' to allow auto-creation of non-existing users
blockRegistration: false # set to 'true' to deny login with SSO without prior registration by an admin
useAsUsername: email # Default is 'email'; custom fields can be used as the username
scopes: openid, profile, email # Specify the scopes for which the application will request permissions
provider: google # Set this to your OAuth provider's name, e.g., 'google' or 'keycloak'
useAsUsername: email # default is 'email'; custom fields can be used as the username
scopes: openid, profile, email # specify the scopes for which the application will request permissions
provider: google # set this to your OAuth provider's name, e.g., 'google' or 'keycloak'
saml2:
enabled: false # currently in alpha, not recommended for use yet, enableAlphaFunctionality must be set to true
autoCreateUser: false # set to 'true' to allow auto-creation of non-existing users
blockRegistration: false # set to 'true' to deny login with SSO without prior registration by an admin
registrationId: stirling
idpMetadataUri: https://dev-XXXXXXXX.okta.com/app/externalKey/sso/saml/metadata
idpSingleLogoutUrl: https://dev-XXXXXXXX.okta.com/app/dev-XXXXXXXX_stirlingpdf_1/externalKey/slo/saml
idpSingleLoginUrl: https://dev-XXXXXXXX.okta.com/app/dev-XXXXXXXX_stirlingpdf_1/externalKey/sso/saml
idpIssuer: http://www.okta.com/externalKey
idpCert: classpath:okta.crt
privateKey: classpath:saml-private-key.key
spCert: classpath:saml-public-cert.crt
enterpriseEdition:
enabled: false # set to 'true' to enable enterprise edition
key: 00000000-0000-0000-0000-000000000000
CustomMetadata:
autoUpdateMetadata: false # set to 'true' to automatically update metadata with below values
author: username # supports text such as 'John Doe' or types such as username to autopopulate with user's username
creator: Stirling-PDF # supports text such as 'Company-PDF'
producer: Stirling-PDF # supports text such as 'Company-PDF'
legal:
termsAndConditions: https://www.stirlingpdf.com/terms-and-conditions # URL to the terms and conditions of your application (e.g. https://example.com/terms). Empty string to disable or filename to load from local file in static folder
privacyPolicy: https://www.stirlingpdf.com/privacy-policy # URL to the privacy policy of your application (e.g. https://example.com/privacy). Empty string to disable or filename to load from local file in static folder
accessibilityStatement: '' # URL to the accessibility statement of your application (e.g. https://example.com/accessibility). Empty string to disable or filename to load from local file in static folder
cookiePolicy: '' # URL to the cookie policy of your application (e.g. https://example.com/cookie). Empty string to disable or filename to load from local file in static folder
impressum: '' # URL to the impressum of your application (e.g. https://example.com/impressum). Empty string to disable or filename to load from local file in static folder
system:
defaultLocale: 'en-US' # Set the default language (e.g. 'de-DE', 'fr-FR', etc)
defaultLocale: en-US # set the default language (e.g. 'de-DE', 'fr-FR', etc)
googlevisibility: false # 'true' to allow Google visibility (via robots.txt), 'false' to disallow
enableAlphaFunctionality: false # Set to enable functionality which might need more testing before it fully goes live (This feature might make no changes)
showUpdate: true # see when a new update is available
showUpdateOnlyAdmin: false # Only admins can see when a new update is available, depending on showUpdate it must be set to 'true'
customHTMLFiles: false # enable to have files placed in /customFiles/templates override the existing template html files
enableAlphaFunctionality: false # set to enable functionality which might need more testing before it fully goes live (this feature might make no changes)
showUpdate: false # see when a new update is available
showUpdateOnlyAdmin: false # only admins can see when a new update is available, depending on showUpdate it must be set to 'true'
customHTMLFiles: false # enable to have files placed in /customFiles/templates override the existing template HTML files
tessdataDir: /usr/share/tessdata # path to the directory containing the Tessdata files. This setting is relevant for Windows systems. For Windows users, this path should be adjusted to point to the appropriate directory where the Tessdata files are stored.
enableAnalytics: undefined # set to 'true' to enable analytics, set to 'false' to disable analytics; for enterprise users, this is set to true
ui:
appName: '' # Application's visible name
homeDescription: '' # Short description or tagline shown on homepage.
appNameNavbar: '' # Name displayed on the navigation bar
appName: '' # application's visible name
homeDescription: '' # short description or tagline shown on the homepage
appNameNavbar: '' # name displayed on the navigation bar
endpoints:
toRemove: [] # List endpoints to disable (e.g. ['img-to-pdf', 'remove-pages'])
groupsToRemove: [] # List groups to disable (e.g. ['LibreOffice'])
toRemove: [] # list endpoints to disable (e.g. ['img-to-pdf', 'remove-pages'])
groupsToRemove: [] # list groups to disable (e.g. ['LibreOffice'])
metrics:
enabled: true # 'true' to enable Info APIs (`/api/*`) endpoints, 'false' to disable
# Automatically Generated Settings (Do Not Edit Directly)
AutomaticallyGenerated:
key: example
UUID: example
```
There is an additional config file ``/configs/custom_settings.yml`` were users familiar with java and spring application.properties can input their own settings on-top of Stirling-PDFs existing ones
There is an additional config file `/configs/custom_settings.yml` where users familiar with Java and Spring `application.properties` can input their own settings on top of Stirling-PDF's existing ones.
### Extra notes
- Endpoints. Currently, the endpoints ENDPOINTS_TO_REMOVE and GROUPS_TO_REMOVE can include comma separate lists of endpoints and groups to disable as example ENDPOINTS_TO_REMOVE=img-to-pdf,remove-pages would disable both image-to-pdf and remove pages, GROUPS_TO_REMOVE=LibreOffice Would disable all things that use LibreOffice. You can see a list of all endpoints and groups [here](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/Endpoint-groups.md)
- customStaticFilePath. Customise static files such as the app logo by placing files in the /customFiles/static/ directory. An example of customising app logo is placing a /customFiles/static/favicon.svg to override current SVG. This can be used to change any images/icons/css/fonts/js etc in Stirling-PDF
### Environment only parameters
### Extra Notes
- ``SYSTEM_ROOTURIPATH`` ie set to ``/pdf-app`` to Set the application's root URI to ``localhost:8080/pdf-app``
- ``SYSTEM_CONNECTIONTIMEOUTMINUTES`` to set custom connection timeout values
- ``DOCKER_ENABLE_SECURITY`` to tell docker to download security jar (required as true for auth login)
- ``INSTALL_BOOK_AND_ADVANCED_HTML_OPS`` to download calibre onto stirling-pdf enabling pdf to/from book and advanced html conversion
- ``LANGS`` to define custom font libraries to install for use for document conversions
- **Endpoints**: Currently, the `ENDPOINTS_TO_REMOVE` and `GROUPS_TO_REMOVE` endpoints can include comma-separated lists of endpoints and groups to disable. For example, `ENDPOINTS_TO_REMOVE=img-to-pdf,remove-pages` would disable both image-to-pdf and remove pages, while `GROUPS_TO_REMOVE=LibreOffice` would disable all things that use LibreOffice. You can see a list of all endpoints and groups [here](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/Endpoint-groups.md).
- **customStaticFilePath**: Customize static files such as the app logo by placing files in the `/customFiles/static/` directory. An example of customizing the app logo is placing `/customFiles/static/favicon.svg` to override the current SVG. This can be used to change any `images/icons/css/fonts/js`, etc. in Stirling-PDF.
### Environment-Only Parameters
- `SYSTEM_ROOTURIPATH` - Set the application's root URI (e.g. `/pdf-app` to set the root URI to `localhost:8080/pdf-app`)
- `SYSTEM_CONNECTIONTIMEOUTMINUTES` - Set custom connection timeout values
- `DOCKER_ENABLE_SECURITY` - Set to `true` to download security jar (required for authentication login)
- `INSTALL_BOOK_AND_ADVANCED_HTML_OPS` - Download Calibre onto Stirling-PDF to enable PDF to/from book and advanced HTML conversion
- `LANGS` - Define custom font libraries to install for document conversions
## API
For those wanting to use Stirling-PDFs backend API to link with their own custom scripting to edit PDFs you can view all existing API documentation
[here](https://app.swaggerhub.com/apis-docs/Stirling-Tools/Stirling-PDF/) or navigate to /swagger-ui/index.html of your stirling-pdf instance for your versions documentation (Or by following the API button in your settings of Stirling-PDF)
For those wanting to use Stirling-PDF's backend API to link with their own custom scripting to edit PDFs, you can view all existing API documentation [here](https://app.swaggerhub.com/apis-docs/Stirling-Tools/Stirling-PDF/), or navigate to `/swagger-ui/index.html` of your Stirling-PDF instance for your version's documentation (or by following the API button in the settings of Stirling-PDF).
## Login authentication
## Login Authentication
![stirling-login](images/login-light.png)
### Prerequisites
- User must have the folder ./configs volumed within docker so that it is retained during updates.
- Docker users must download the security jar version by setting ``DOCKER_ENABLE_SECURITY`` to ``true`` in environment variables.
- Then either enable login via the settings.yml file or via setting ``SECURITY_ENABLE_LOGIN`` to ``true``
- Now the initial user will be generated with username ``admin`` and password ``stirling``. On login you will be forced to change the password to a new one. You can also use the environment variables ``SECURITY_INITIALLOGIN_USERNAME`` and ``SECURITY_INITIALLOGIN_PASSWORD`` to set your own straight away (Recommended to remove them after user creation).
- User must have the folder `./configs` volumed within Docker so that it is retained during updates.
- Docker users must download the security jar version by setting `DOCKER_ENABLE_SECURITY` to `true` in environment variables.
- Then either enable login via the `settings.yml` file or set `SECURITY_ENABLE_LOGIN` to `true`.
- Now the initial user will be generated with username `admin` and password `stirling`. On login, you will be forced to change the password to a new one. You can also use the environment variables `SECURITY_INITIALLOGIN_USERNAME` and `SECURITY_INITIALLOGIN_PASSWORD` to set your own credentials straight away (recommended to remove them after user creation).
Once the above has been done, on restart, a new stirling-pdf-DB.mv.db will show if everything worked.
Once the above has been done, on restart, a new `stirling-pdf-DB.mv.db` will show if everything worked.
When you login to Stirling PDF you will be redirected to /login page to login with those default credentials. After login everything should function as normal
When you log in to Stirling-PDF, you will be redirected to the `/login` page to log in with those default credentials. After login, everything should function as normal.
To access your account settings go to Account settings in the settings cog menu (top right in navbar) This Account settings menu is also where you find your API key.
To access your account settings, go to Account Settings in the settings cog menu (top right in the navbar). This Account Settings menu is also where you find your API key.
To add new users go to the bottom of Account settings and hit 'Admin Settings', here you can add new users. The different roles mentioned within this are for rate limiting. This is a Work in progress which will be expanding on more in future
To add new users, go to the bottom of Account Settings and hit 'Admin Settings'. Here you can add new users. The different roles mentioned within this are for rate limiting. This is a work in progress and will be expanded on more in the future.
For API usage you must provide a header with 'X-API-Key' and the associated API key for that user.
For API usage, you must provide a header with `X-API-Key` and the associated API key for that user.
## FAQ
### Q1: What are your planned features?
- Progress bar/Tracking
- Full custom logic pipelines to combine multiple operations together.
- Folder support with auto scanning to perform operations on
- Redact text (Via UI not just automated way)
- Add Forms
- Multi page layout (Stich PDF pages together) support x rows y columns and custom page sizing
- Progress bar/tracking
- Full custom logic pipelines to combine multiple operations together
- Folder support with auto-scanning to perform operations on
- Redact text (via UI, not just automated)
- Add forms
- Multi-page layout (stitch PDF pages together) support x rows y columns and custom page sizing
- Fill forms manually or automatically
### Q2: Why is my application downloading .htm files?
### Q2: Why is my application downloading .htm files? Why am i getting HTTP error 413?
This is an issue caused commonly by your NGINX configuration. The default file upload size for NGINX is 1MB, you need to add the following in your Nginx sites-available file. ``client_max_body_size SIZE;`` Where "SIZE" is 50M for example for 50MB files.
This is an issue commonly caused by your NGINX configuration. The default file upload size for NGINX is 1MB. You need to add the following in your Nginx sites-available file: `client_max_body_size SIZE;` (where "SIZE" is 50M for example for 50MB files).
### Q3: Why is my download timing out
### Q3: Why is my download timing out?
NGINX has timeout values by default so if you are running Stirling-PDF behind NGINX you may need to set a timeout value such as adding the config ``proxy_read_timeout 3600;``
NGINX has timeout values by default, so if you are running Stirling-PDF behind NGINX, you may need to set a timeout value, such as adding the config `proxy_read_timeout 3600;`.

View File

@@ -1,14 +1,14 @@
|All versions in a Docker environment can download Calibre as a optional extra at runtime to support `book-to-pdf` and `pdf-to-book` using parameter ``INSTALL_BOOK_AND_ADVANCED_HTML_OPS``.
The 'Fat' container contains all those found in 'Full' with security jar along with this Calibre install.
The 'Fat' container contains all those found in 'Full' with security jar along with this Calibre install.
Technology | Ultra-Lite | Full |
| Technology | Ultra-Lite | Full |
| ---------- | :--------: | :---: |
| Java | ✔️ | ✔️ |
| JavaScript | ✔️ | ✔️ |
| Libre | | ✔️ |
| Python | | ✔️ |
| OpenCV | | ✔️ |
| OCRmyPDF | | ✔️ |
| qpdf | | ✔️ |
| Operation | Ultra-Lite | Full |
| ---------------------- | ---------- | ---- |
@@ -54,3 +54,15 @@ Technology | Ultra-Lite | Full |
| ocr-pdf | | ✔️ |
| pdf-to-pdfa | | ✔️ |
| remove-blanks | | ✔️ |
pdf-to-text | ✔️ | ✔️
pdf-to-html | | ✔️
pdf-to-word | | ✔️
pdf-to-presentation | | ✔️
pdf-to-xml | | ✔️
remove-annotations | ✔️ | ✔️
remove-cert-sign | ✔️ | ✔️
remove-image-pdf | ✔️ | ✔️
file-to-pdf | | ✔️
html-to-pdf | | ✔️
url-to-pdf | | ✔️
repair | | ✔️

View File

@@ -1,6 +1,6 @@
plugins {
id "java"
id "org.springframework.boot" version "3.3.5"
id "org.springframework.boot" version "3.4.0"
id "io.spring.dependency-management" version "1.1.6"
id "org.springdoc.openapi-gradle-plugin" version "1.8.0"
id "io.swagger.swaggerhub" version "1.3.2"
@@ -10,19 +10,24 @@ plugins {
//id "nebula.lint" version "19.0.3"
}
import com.github.jk1.license.render.*
ext {
springBootVersion = "3.3.5"
springBootVersion = "3.4.0"
pdfboxVersion = "3.0.3"
logbackVersion = "1.5.7"
imageioVersion = "3.12.0"
lombokVersion = "1.18.34"
bouncycastleVersion = "1.78.1"
lombokVersion = "1.18.36"
bouncycastleVersion = "1.79"
springSecuritySamlVersion = "6.4.1"
openSamlVersion = "4.3.2"
}
group = "stirling.software"
version = "0.31.0"
version = "0.36.0"
java {
// 17 is lowest but we support and recommend 21
@@ -78,7 +83,7 @@ launch4j {
errTitle="Encountered error, Do you have Java 21?"
downloadUrl="https://download.oracle.com/java/21/latest/jdk-21_windows-x64_bin.exe"
variables=["BROWSER_OPEN=true", "ENDPOINTS_GROUPS_TO_REMOVE=CLI"]
variables=["BROWSER_OPEN=true"]
jreMinVersion="17"
mutexName="Stirling-PDF"
@@ -119,7 +124,7 @@ configurations.all {
}
dependencies {
//security updates
implementation "org.springframework:spring-webmvc:6.1.14"
implementation "org.springframework:spring-webmvc:6.2.0"
implementation("io.github.pixee:java-security-toolkit:1.2.0")
@@ -141,18 +146,18 @@ dependencies {
implementation "org.springframework.boot:spring-boot-starter-data-jpa:$springBootVersion"
implementation "org.springframework.boot:spring-boot-starter-oauth2-client:$springBootVersion"
implementation 'org.springframework.security:spring-security-saml2-service-provider:6.3.4'
implementation "org.springframework.session:spring-session-core:$springBootVersion"
implementation 'com.unboundid.product.scim2:scim2-sdk-client:2.3.5'
//2.2.x requires rebuild of DB file.. need migration path
runtimeOnly "com.h2database:h2:2.1.214"
// implementation "com.h2database:h2:2.2.224"
// Don't upgrade h2database
runtimeOnly "com.h2database:h2:2.3.232"
constraints {
implementation "org.opensaml:opensaml-core"
implementation "org.opensaml:opensaml-saml-api"
implementation "org.opensaml:opensaml-saml-impl"
implementation "org.opensaml:opensaml-core:$openSamlVersion"
implementation "org.opensaml:opensaml-saml-api:$openSamlVersion"
implementation "org.opensaml:opensaml-saml-impl:$openSamlVersion"
}
implementation "org.springframework.security:spring-security-saml2-service-provider"
implementation "org.springframework.security:spring-security-saml2-service-provider:$springSecuritySamlVersion"
// implementation 'org.springframework.security:spring-security-core:$springSecuritySamlVersion'
implementation 'com.coveo:saml-client:5.0.0'
@@ -184,7 +189,7 @@ dependencies {
// Image metadata extractor
implementation "com.drewnoakes:metadata-extractor:2.19.0"
implementation "commons-io:commons-io:2.17.0"
implementation "commons-io:commons-io:2.18.0"
implementation "org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0"
//general PDF
@@ -201,12 +206,19 @@ dependencies {
exclude group: "commons-logging", module: "commons-logging"
}
// https://mvnrepository.com/artifact/technology.tabula/tabula
implementation ('technology.tabula:tabula:1.0.5') {
exclude group: "org.slf4j", module: "slf4j-simple"
exclude group: "org.bouncycastle", module: "bcprov-jdk15on"
exclude group: "com.google.code.gson", module: "gson"
}
implementation 'org.apache.pdfbox:jbig2-imageio:3.0.4'
implementation "org.bouncycastle:bcprov-jdk18on:$bouncycastleVersion"
implementation "org.bouncycastle:bcpkix-jdk18on:$bouncycastleVersion"
implementation "org.springframework.boot:spring-boot-starter-actuator:$springBootVersion"
implementation "io.micrometer:micrometer-core:1.13.6"
implementation "io.micrometer:micrometer-core:1.14.1"
implementation group: "com.google.zxing", name: "core", version: "3.5.3"
// https://mvnrepository.com/artifact/org.commonmark/commonmark
implementation "org.commonmark:commonmark:0.24.0"

View File

@@ -1,16 +0,0 @@
apiVersion: v2
appVersion: 0.31.0
description: locally hosted web application that allows you to perform various operations
on PDF files
home: https://github.com/Stirling-Tools/Stirling-PDF
keywords:
- stirling-pdf
- helm
- charts repo
maintainers:
- name: Stirling-Tools
url: https://github.com/Stirling-Tools/Stirling-PDF
name: stirling-pdf-chart
sources:
- https://github.com/Stirling-Tools/Stirling-PDF
version: 1.0.0

View File

@@ -1,30 +0,0 @@
** Please be patient while the chart is being deployed **
Get the stirlingpdf URL by running:
{{- if contains "NodePort" .Values.service.type }}
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ template "stirlingpdf.fullname" . }})
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
echo http://$NODE_IP:$NODE_PORT/
{{- else if contains "LoadBalancer" .Values.service.type }}
** Please ensure an external IP is associated to the {{ template "stirlingpdf.fullname" . }} service before proceeding **
** Watch the status using: kubectl get svc --namespace {{ .Release.Namespace }} -w {{ template "stirlingpdf.fullname" . }} **
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ template "stirlingpdf.fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
echo http://$SERVICE_IP:{{ .Values.service.externalPort }}/
OR
export SERVICE_HOST=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ template "stirlingpdf.fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].hostname}')
echo http://$SERVICE_HOST:{{ .Values.service.externalPort }}/
{{- else if contains "ClusterIP" .Values.service.type }}
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app={{ template "stirlingpdf.name" . }}" -l "release={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
echo http://127.0.0.1:8080/
kubectl port-forward $POD_NAME 8080:8080 --namespace {{ .Release.Namespace }}
{{- end }}

View File

@@ -1,129 +0,0 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "stirlingpdf.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "stirlingpdf.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{- /*
Create chart name and version as used by the chart label.
It does minimal escaping for use in Kubernetes labels.
Example output:
stirlingpdf-0.4.5
*/ -}}
{{- define "stirlingpdf.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end -}}
{{/*
Common labels
*/}}
{{- define "stirlingpdf.labels" -}}
helm.sh/chart: {{ include "stirlingpdf.chart" . }}
{{ include "stirlingpdf.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
{{- if .Values.commonLabels}}
{{ toYaml .Values.commonLabels }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "stirlingpdf.selectorLabels" -}}
app.kubernetes.io/name: {{ include "stirlingpdf.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "stirlingpdf.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "stirlingpdf.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}
{{/*
Return the proper image name to change the volume permissions
*/}}
{{- define "stirlingpdf.volumePermissions.image" -}}
{{- $registryName := .Values.volumePermissions.image.registry -}}
{{- $repositoryName := .Values.volumePermissions.image.repository -}}
{{- $tag := .Values.volumePermissions.image.tag | toString -}}
{{/*
Helm 2.11 supports the assignment of a value to a variable defined in a different scope,
but Helm 2.9 and 2.10 doesn't support it, so we need to implement this if-else logic.
Also, we can't use a single if because lazy evaluation is not an option
*/}}
{{- if .Values.global }}
{{- if .Values.global.imageRegistry }}
{{- printf "%s/%s:%s" .Values.global.imageRegistry $repositoryName $tag -}}
{{- else -}}
{{- printf "%s/%s:%s" $registryName $repositoryName $tag -}}
{{- end -}}
{{- else -}}
{{- printf "%s/%s:%s" $registryName $repositoryName $tag -}}
{{- end -}}
{{- end -}}
{{/*
Return the proper Docker Image Registry Secret Names
*/}}
{{- define "stirlingpdf.imagePullSecrets" -}}
{{/*
Helm 2.11 supports the assignment of a value to a variable defined in a different scope,
but Helm 2.9 and 2.10 does not support it, so we need to implement this if-else logic.
Also, we can not use a single if because lazy evaluation is not an option
*/}}
{{- if .Values.global }}
{{- if .Values.global.imagePullSecrets }}
imagePullSecrets:
{{- range .Values.global.imagePullSecrets }}
- name: {{ . }}
{{- end }}
{{- else if or .Values.image.pullSecrets .Values.volumePermissions.image.pullSecrets }}
imagePullSecrets:
{{- range .Values.image.pullSecrets }}
- name: {{ . }}
{{- end }}
{{- range .Values.volumePermissions.image.pullSecrets }}
- name: {{ . }}
{{- end }}
{{- end -}}
{{- else if or .Values.image.pullSecrets .Values.volumePermissions.image.pullSecrets }}
imagePullSecrets:
{{- range .Values.image.pullSecrets }}
- name: {{ . }}
{{- end }}
{{- range .Values.volumePermissions.image.pullSecrets }}
- name: {{ . }}
{{- end }}
{{- end -}}
{{- end -}}

View File

@@ -1,131 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "stirlingpdf.fullname" . }}
{{- with .Values.deployment.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
labels:
{{- include "stirlingpdf.labels" . | nindent 4 }}
{{- if .Values.deployment.labels }}
{{- toYaml .Values.deployment.labels | nindent 4 }}
{{- end }}
spec:
selector:
matchLabels:
{{- include "stirlingpdf.selectorLabels" . | nindent 6 }}
replicas: {{ .Values.replicaCount }}
strategy:
{{ toYaml .Values.strategy | indent 4 }}
revisionHistoryLimit: 10
template:
metadata:
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "stirlingpdf.selectorLabels" . | nindent 8 }}
{{- if .Values.podLabels }}
{{- toYaml .Values.podLabels | nindent 8 }}
{{- end }}
spec:
{{- if .Values.priorityClassName }}
priorityClassName: "{{ .Values.priorityClassName }}"
{{- end }}
{{- if .Values.securityContext.enabled }}
securityContext:
fsGroup: {{ .Values.securityContext.fsGroup }}
{{- if .Values.securityContext.runAsNonRoot }}
runAsNonRoot: {{ .Values.securityContext.runAsNonRoot }}
{{- end }}
{{- if .Values.securityContext.supplementalGroups }}
supplementalGroups: {{ .Values.securityContext.supplementalGroups }}
{{- end }}
{{- else if .Values.persistence.enabled }}
initContainers:
- name: volume-permissions
image: {{ template "stirlingpdf.volumePermissions.image" . }}
imagePullPolicy: "{{ .Values.volumePermissions.image.pullPolicy }}"
securityContext:
{{- toYaml .Values.containerSecurityContext | nindent 10 }}
command: ['sh', '-c', 'chown -R {{ .Values.securityContext.fsGroup }}:{{ .Values.securityContext.fsGroup }} {{ .Values.persistence.path }}']
volumeMounts:
- mountPath: {{ .Values.persistence.path }}
name: storage-volume
{{- end }}
{{- include "stirlingpdf.imagePullSecrets" . | indent 6 }}
containers:
- name: {{ .Chart.Name }}
image: {{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}
imagePullPolicy: {{ .Values.image.pullPolicy }}
securityContext:
{{- toYaml .Values.containerSecurityContext | nindent 10 }}
env:
- name: SYSTEM_ROOTURIPATH
value: {{ .Values.rootPath}}
{{- if .Values.envs }}
{{ toYaml .Values.envs | indent 8 }}
{{- end }}
{{- if .Values.extraArgs }}
args:
{{ toYaml .Values.extraArgs | indent 8 }}
{{- end }}
ports:
- name: http
containerPort: 8080
livenessProbe:
httpGet:
path: {{ .Values.rootPath}}
port: http
{{ toYaml .Values.probes.livenessHttpGetConfig | indent 12 }}
{{ toYaml .Values.probes.liveness | indent 10 }}
readinessProbe:
httpGet:
path: {{ .Values.rootPath}}
port: http
{{ toYaml .Values.probes.readinessHttpGetConfig | indent 12 }}
{{ toYaml .Values.probes.readiness | indent 10 }}
volumeMounts:
{{- if .Values.deployment.extraVolumeMounts }}
{{- toYaml .Values.deployment.extraVolumeMounts | nindent 8 }}
{{- end }}
{{- if .Values.deployment.sidecarContainers }}
{{- range $name, $spec := .Values.deployment.sidecarContainers }}
- name: {{ $name }}
{{- toYaml $spec | nindent 8 }}
{{- end }}
{{- end }}
{{- with .Values.resources }}
resources:
{{ toYaml . | indent 10 }}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{ toYaml . | indent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{ toYaml . | indent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{ toYaml . | indent 8 }}
{{- end }}
{{- if .Values.schedulerName }}
schedulerName: {{ .Values.schedulerName }}
{{- end }}
serviceAccountName: {{ include "stirlingpdf.serviceAccountName" . }}
automountServiceAccountToken: {{ .Values.serviceAccount.automountServiceAccountToken }}
volumes:
{{- if .Values.deployment.extraVolumes }}
{{- toYaml .Values.deployment.extraVolumes | nindent 6 }}
{{- end }}
- name: storage-volume
{{- if .Values.persistence.enabled }}
persistentVolumeClaim:
claimName: {{ .Values.persistence.existingClaim | default (include "stirlingpdf.fullname" .) }}
{{- else }}
emptyDir: {}
{{- end }}

View File

@@ -1,85 +0,0 @@
{{- if .Values.ingress.enabled }}
{{- $servicePort := .Values.service.externalPort -}}
{{- $serviceName := include "stirlingpdf.fullname" . -}}
{{- $ingressExtraPaths := .Values.ingress.extraPaths -}}
---
{{- if semverCompare "<1.14-0" .Capabilities.KubeVersion.GitVersion }}
apiVersion: extensions/v1beta1
{{- else if semverCompare "<1.19-0" .Capabilities.KubeVersion.GitVersion }}
apiVersion: networking.k8s.io/v1beta1
{{- else }}
apiVersion: networking.k8s.io/v1
{{- end }}
kind: Ingress
metadata:
name: {{ include "stirlingpdf.fullname" . }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
labels:
{{- include "stirlingpdf.labels" . | nindent 4 }}
{{- with .Values.ingress.labels }}
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- with .Values.ingress.ingressClassName }}
ingressClassName: {{ . }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .name }}
http:
paths:
{{- range $ingressExtraPaths }}
- path: {{ default "/" .path | quote }}
backend:
{{- if semverCompare "<1.19-0" $.Capabilities.KubeVersion.GitVersion }}
{{- if $.Values.service.servicename }}
serviceName: {{ $.Values.service.servicename }}
{{- else }}
serviceName: {{ default $serviceName .service }}
{{- end }}
servicePort: {{ default $servicePort .port }}
{{- else }}
service:
{{- if $.Values.service.servicename }}
name: {{ $.Values.service.servicename }}
{{- else }}
name: {{ default $serviceName .service }}
{{- end }}
port:
number: {{ default $servicePort .port }}
pathType: {{ default $.Values.ingress.pathType .pathType }}
{{- end }}
{{- end }}
- path: {{ default "/" .path | quote }}
backend:
{{- if semverCompare "<1.19-0" $.Capabilities.KubeVersion.GitVersion }}
{{- if $.Values.service.servicename }}
serviceName: {{ $.Values.service.servicename }}
{{- else }}
serviceName: {{ default $serviceName .service }}
{{- end }}
servicePort: {{ default $servicePort .servicePort }}
{{- else }}
service:
{{- if $.Values.service.servicename }}
name: {{ $.Values.service.servicename }}
{{- else }}
name: {{ default $serviceName .service }}
{{- end }}
port:
number: {{ default $servicePort .port }}
pathType: {{ $.Values.ingress.pathType }}
{{- end }}
{{- end }}
tls:
{{- range .Values.ingress.hosts }}
{{- if .tls }}
- hosts:
- {{ .name }}
secretName: {{ .tlsSecret }}
{{- end }}
{{- end }}
{{- end -}}

View File

@@ -1,16 +0,0 @@
{{- if .Values.persistence.pv.enabled -}}
apiVersion: v1
kind: PersistentVolume
metadata:
name: {{ .Values.persistence.pv.pvname | default (include "stirlingpdf.fullname" .) }}
labels:
{{- include "stirlingpdf.labels" . | nindent 4 }}
spec:
capacity:
storage: {{ .Values.persistence.pv.capacity.storage }}
accessModes:
- {{ .Values.persistence.pv.accessMode | quote }}
nfs:
server: {{ .Values.persistence.pv.nfs.server }}
path: {{ .Values.persistence.pv.nfs.path | quote }}
{{- end }}

View File

@@ -1,27 +0,0 @@
{{- if and .Values.persistence.enabled (not .Values.persistence.existingClaim) -}}
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: {{ include "stirlingpdf.fullname" . }}
labels:
{{- include "stirlingpdf.labels" . | nindent 4 }}
{{- with .Values.persistence.labels }}
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
accessModes:
- {{ .Values.persistence.accessMode | quote }}
resources:
requests:
storage: {{ .Values.persistence.size | quote }}
{{- if .Values.persistence.storageClass }}
{{- if (eq "-" .Values.persistence.storageClass) }}
storageClassName: ""
{{- else }}
storageClassName: "{{ .Values.persistence.storageClass }}"
{{- end }}
{{- if .Values.persistence.volumeName }}
volumeName: "{{ .Values.persistence.volumeName }}"
{{- end }}
{{- end }}
{{- end }}

View File

@@ -1,48 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: {{ .Values.service.servicename | default (include "stirlingpdf.fullname" .) }}
{{- with .Values.service.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
labels:
{{- include "stirlingpdf.labels" . | nindent 4 }}
{{- with .Values.service.labels }}
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
type: {{ .Values.service.type }}
{{- if (or (eq .Values.service.type "LoadBalancer") (and (eq .Values.service.type "NodePort") (not (empty .Values.service.nodePort)))) }}
externalTrafficPolicy: {{ .Values.service.externalTrafficPolicy }}
{{- end }}
{{- if (and (eq .Values.service.type "LoadBalancer") .Values.service.loadBalancerIP) }}
loadBalancerIP: {{ .Values.service.loadBalancerIP }}
{{- end }}
{{- if (and (eq .Values.service.type "LoadBalancer") .Values.service.loadBalancerSourceRanges) }}
loadBalancerSourceRanges:
{{- with .Values.service.loadBalancerSourceRanges }}
{{ toYaml . | indent 2 }}
{{- end }}
{{- end }}
{{- if eq .Values.service.type "ClusterIP" }}
{{- if .Values.service.clusterIP }}
clusterIP: {{ .Values.service.clusterIP }}
{{- end }}
{{- end }}
ports:
- port: {{ .Values.service.externalPort }}
{{- if (and (eq .Values.service.type "NodePort") (not (empty .Values.service.nodePort))) }}
nodePort: {{.Values.service.nodePort}}
{{- end }}
{{- if .Values.service.targetPort }}
targetPort: {{ .Values.service.targetPort }}
name: {{ .Values.service.targetPort }}
{{- else }}
targetPort: http
name: http
{{- end }}
protocol: TCP
selector:
{{- include "stirlingpdf.selectorLabels" . | nindent 4 }}

View File

@@ -1,13 +0,0 @@
{{- if .Values.serviceAccount.create -}}
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "stirlingpdf.serviceAccountName" . }}
{{- with .Values.serviceAccount.annotations }}
annotations:
{{ toYaml . | nindent 4 }}
{{- end }}
labels:
{{- include "stirlingpdf.labels" . | nindent 4 }}
{{- end }}

View File

@@ -1,31 +0,0 @@
{{- if and ( .Capabilities.APIVersions.Has "monitoring.coreos.com/v1" ) ( .Values.serviceMonitor.enabled ) }}
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: {{ include "stirlingpdf.fullname" . }}
namespace: {{ .Values.serviceMonitor.namespace | default .Release.Namespace }}
labels:
{{- include "stirlingpdf.labels" . | nindent 4 }}
{{- with .Values.serviceMonitor.labels }}
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
endpoints:
- targetPort: 8080
{{- if .Values.serviceMonitor.interval }}
interval: {{ .Values.serviceMonitor.interval }}
{{- end }}
{{- if .Values.serviceMonitor.metricsPath }}
path: {{ .Values.serviceMonitor.metricsPath }}
{{- end }}
{{- if .Values.serviceMonitor.timeout }}
scrapeTimeout: {{ .Values.serviceMonitor.timeout }}
{{- end }}
jobLabel: {{ include "stirlingpdf.fullname" . }}
namespaceSelector:
matchNames:
- {{ .Release.Namespace }}
selector:
matchLabels:
{{- include "stirlingpdf.selectorLabels" . | nindent 6 }}
{{- end }}

View File

@@ -1,240 +0,0 @@
extraArgs: []
# - --storage-timestamp-tolerance 1s
replicaCount: 1
strategy:
type: RollingUpdate
image:
repository: frooodle/s-pdf
# took Chart appVersion by default
tag: ~
pullPolicy: IfNotPresent
secret:
labels: {}
## Labels to apply to all resources
##
commonLabels: {}
# team_name: dev
# rootpath for the application
rootPath: /
envs: []
# - name: UI_APP_NAME
# value: "Stirling PDF"
# - name: UI_HOME_DESCRIPTION
# value: "Your locally hosted one-stop-shop for all your PDF needs."
# - name: UI_APP_NAVBAR_NAME
# value: "Stirling PDF"
# - name: ALLOW_GOOGLE_VISIBILITY
# value: "true"
# - name: APP_LOCALE
# value: "en_GB"
deployment:
## stirling-pdf Deployment annotations
annotations: {}
# name: value
labels: {}
# name: value
# additional volumes
extraVolumes: []
# - name: nginx-config
# secret:
# secretName: nginx-config
# additional volumes to mount
extraVolumeMounts: []
## sidecarContainers for the stirling-pdf
# Can be used to add a proxy to the pod that does
# scanning for secrets, signing, authentication, validation
# of the chart's content, send notifications...
sidecarContainers: {}
## Example sidecarContainer which uses an extraVolume from above and
## a named port that can be referenced in the service as targetPort.
# proxy:
# image: nginx:latest
# ports:
# - name: proxy
# containerPort: 8081
# volumeMounts:
# - name: nginx-config
# readOnly: true
# mountPath: /etc/nginx
## Pod annotations
## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/
## Read more about kube2iam to provide access to s3 https://github.com/jtblin/kube2iam
##
podAnnotations: {}
# iam.amazonaws.com/role: role-arn
## Pod labels
## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/
podLabels: {}
# name: value
service:
servicename:
type: ClusterIP
externalTrafficPolicy: Local
## Uses pre-assigned IP address from cloud provider
## Only valid if service.type: LoadBalancer
loadBalancerIP:
## Limits which cidr blocks can connect to service's load balancer
## Only valid if service.type: LoadBalancer
loadBalancerSourceRanges: []
# clusterIP: None
externalPort: 8080
## targetPort of the container to use. If a sidecar should handle the
## requests first, use the named port from the sidecar. See sidecar example
## from deployment above. Leave empty to use stirling-pdf directly.
targetPort:
nodePort:
annotations: {}
labels: {}
serviceMonitor:
enabled: false
# namespace: prometheus
labels: {}
metricsPath: "/metrics"
# timeout: 60
# interval: 60
resources: {}
# limits:
# cpu: 100m
# memory: 128Mi
# requests:
# cpu: 80m
# memory: 64Mi
probes:
liveness:
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 1
successThreshold: 1
failureThreshold: 3
livenessHttpGetConfig:
scheme: HTTP
readiness:
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 1
successThreshold: 1
failureThreshold: 3
readinessHttpGetConfig:
scheme: HTTP
serviceAccount:
create: true
name: ""
automountServiceAccountToken: false
## Annotations for the Service Account
annotations: {}
# UID/GID 1000 is the default user "stirling-pdf" used in
# the container image starting in v0.8.0 and above. This
# is required for local persistent storage. If your cluster
# does not allow this, try setting securityContext: {}
securityContext:
enabled: true
fsGroup: 1000
## Optionally, specify supplementalGroups and/or
## runAsNonRoot for security purposes
# runAsNonRoot: true
# supplementalGroups: [1000]
containerSecurityContext: {}
priorityClassName: ""
nodeSelector: {}
tolerations: []
affinity: {}
persistence:
enabled: false
accessMode: ReadWriteOnce
size: 8Gi
labels: {}
# name: value
path: /tmp
## A manually managed Persistent Volume and Claim
## Requires persistence.enabled: true
## If defined, PVC must be created manually before volume will be bound
# existingClaim:
## stirling-pdf data Persistent Volume Storage Class
## If defined, storageClassName: <storageClass>
## If set to "-", storageClassName: "", which disables dynamic provisioning
## If undefined (the default) or set to null, no storageClassName spec is
## set, choosing the default provisioner. (gp2 on AWS, standard on
## GKE, AWS & OpenStack)
##
# storageClass: "-"
# volumeName:
pv:
enabled: false
pvname:
capacity:
storage: 8Gi
accessMode: ReadWriteOnce
nfs:
server:
path:
## Init containers parameters:
## volumePermissions: Change the owner of the persistent volume mountpoint to RunAsUser:fsGroup
##
volumePermissions:
image:
registry: docker.io
repository: bitnami/minideb
tag: buster
pullPolicy: Always
## Optionally specify an array of imagePullSecrets.
## Secrets must be manually created in the namespace.
## ref: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/
##
# pullSecrets:
# - myRegistryKeySecretName
## Ingress for load balancer
ingress:
enabled: false
pathType: "ImplementationSpecific"
## stirling-pdf Ingress labels
##
labels: {}
# dns: "route53"
## stirling-pdf Ingress annotations
##
annotations: {}
# kubernetes.io/ingress.class: nginx
# kubernetes.io/tls-acme: "true"
## stirling-pdf Ingress hostnames
## Must be provided if Ingress is enabled
##
hosts: []
# - name: stirling-pdf.domain1.com
# path: /
# tls: false
# - name: stirling-pdf.domain2.com
# path: /
#
# ## Set this to true in order to enable TLS on the ingress record
# tls: true
#
# ## If TLS is set to true, you must declare what secret will store the key/certificate for TLS
# ## Secrets must be added manually to the namespace
# tlsSecret: stirling-pdf.domain2-tls
# For Kubernetes >= 1.18 you should specify the ingress-controller via the field ingressClassName
# See https://kubernetes.io/blog/2020/04/02/improvements-to-the-ingress-api-in-kubernetes-1.18/#specifying-the-class-of-an-ingress
ingressClassName:

View File

@@ -48,24 +48,6 @@ Feature: API Validation
And the response status code should be 200
@ocr @negative
Scenario: Process PDF with text and OCR with type normal
Given I generate a PDF file as "fileInput"
And the pdf contains 3 pages with random text
And the request data includes
| parameter | value |
| languages | eng |
| sidecar | false |
| deskew | true |
| clean | true |
| cleanFinal | true |
| ocrType | Normal |
| ocrRenderType | hocr |
| removeImagesAfter| false |
When I send the API request to the endpoint "/api/v1/misc/ocr-pdf"
Then the response status code should be 500
@ocr @positive
Scenario: Process PDF with OCR
Given I generate a PDF file as "fileInput"
@@ -83,26 +65,6 @@ Feature: API Validation
Then the response content type should be "application/pdf"
And the response file should have size greater than 0
And the response status code should be 200
@ocr @positive
Scenario: Process PDF with OCR with sidecar
Given I generate a PDF file as "fileInput"
And the request data includes
| parameter | value |
| languages | eng |
| sidecar | true |
| deskew | true |
| clean | true |
| cleanFinal | true |
| ocrType | Force |
| ocrRenderType | hocr |
| removeImagesAfter| false |
When I send the API request to the endpoint "/api/v1/misc/ocr-pdf"
Then the response content type should be "application/octet-stream"
And the response file should have extension ".zip"
And the response ZIP should contain 2 files
And the response file should have size greater than 0
And the response status code should be 200
@libre @positive
@@ -145,7 +107,7 @@ Feature: API Validation
And the response file should have extension ".pdf"
And the response file should have size greater than 100
@compress @ghostscript @positive
@compress @qpdf @positive
Scenario: Compress
Given I use an example file at "exampleFiles/ghost3.pdf" as parameter "fileInput"
And the request data includes
@@ -156,7 +118,7 @@ Feature: API Validation
And the response file should have extension ".pdf"
And the response file should have size greater than 100
@compress @ghostscript @positive
@compress @qpdf @positive
Scenario: Compress
Given I use an example file at "exampleFiles/ghost2.pdf" as parameter "fileInput"
And the request data includes
@@ -169,7 +131,7 @@ Feature: API Validation
And the response file should have size greater than 100
@compress @ghostscript @positive
@compress @qpdf @positive
Scenario: Compress
Given I use an example file at "exampleFiles/ghost1.pdf" as parameter "fileInput"
And the request data includes

View File

@@ -1,7 +1,7 @@
services:
stirling-pdf:
container_name: Stirling-PDF-Security-Fat
image: frooodle/s-pdf:latest-fat
image: stirlingtools/stirling-pdf:latest-fat
deploy:
resources:
limits:
@@ -14,14 +14,17 @@ services:
ports:
- 8080:8080
volumes:
- /stirling/latest/data:/usr/share/tessdata:rw
- /stirling/latest/config:/configs:rw
- /stirling/latest/logs:/logs:rw
- ./stirling/latest/data:/usr/share/tessdata:rw
- ./stirling/latest/config:/configs:rw
- ./stirling/latest/logs:/logs:rw
user: "stirlingpdfuser"
environment:
DOCKER_ENABLE_SECURITY: "true"
SECURITY_ENABLELOGIN: "false"
PUID: 1002
PGID: 1002
LANGS: "ALL"
TESSERACT_LANGS: "eng,fra,deu,spa,ita"
UMASK: "022"
SYSTEM_DEFAULTLOCALE: en-US
UI_APPNAME: Stirling-PDF

View File

@@ -1,7 +1,7 @@
services:
stirling-pdf:
container_name: Stirling-PDF-Security
image: frooodle/s-pdf:latest
image: stirlingtools/stirling-pdf:latest
deploy:
resources:
limits:
@@ -14,9 +14,9 @@ services:
ports:
- "8080:8080"
volumes:
- /stirling/latest/data:/usr/share/tessdata:rw
- /stirling/latest/config:/configs:rw
- /stirling/latest/logs:/logs:rw
- ./stirling/latest/data:/usr/share/tessdata:rw
- ./stirling/latest/config:/configs:rw
- ./stirling/latest/logs:/logs:rw
environment:
DOCKER_ENABLE_SECURITY: "true"
SECURITY_ENABLELOGIN: "true"

View File

@@ -1,7 +1,7 @@
services:
stirling-pdf:
container_name: Stirling-PDF-Security
image: frooodle/s-pdf:latest
image: stirlingtools/stirling-pdf:latest
deploy:
resources:
limits:
@@ -14,9 +14,9 @@ services:
ports:
- "8080:8080"
volumes:
- /stirling/latest/data:/usr/share/tessdata:rw
- /stirling/latest/config:/configs:rw
- /stirling/latest/logs:/logs:rw
- ./stirling/latest/data:/usr/share/tessdata:rw
- ./stirling/latest/config:/configs:rw
- ./stirling/latest/logs:/logs:rw
environment:
DOCKER_ENABLE_SECURITY: "true"
SECURITY_ENABLELOGIN: "true"

View File

@@ -1,7 +1,7 @@
services:
stirling-pdf:
container_name: Stirling-PDF-Ultra-Lite-Security
image: frooodle/s-pdf:latest-ultra-lite
image: stirlingtools/stirling-pdf:latest-ultra-lite
deploy:
resources:
limits:
@@ -14,9 +14,9 @@ services:
ports:
- "8080:8080"
volumes:
- /stirling/latest/data:/usr/share/tessdata:rw
- /stirling/latest/config:/configs:rw
- /stirling/latest/logs:/logs:rw
- ./stirling/latest/data:/usr/share/tessdata:rw
- ./stirling/latest/config:/configs:rw
- ./stirling/latest/logs:/logs:rw
environment:
DOCKER_ENABLE_SECURITY: "true"
SECURITY_ENABLELOGIN: "true"

View File

@@ -1,7 +1,7 @@
services:
stirling-pdf:
container_name: Stirling-PDF-Ultra-Lite
image: frooodle/s-pdf:latest-ultra-lite
image: stirlingtools/stirling-pdf:latest-ultra-lite
deploy:
resources:
limits:
@@ -14,8 +14,8 @@ services:
ports:
- "8080:8080"
volumes:
- /stirling/latest/config:/configs:rw
- /stirling/latest/logs:/logs:rw
- ./stirling/latest/config:/configs:rw
- ./stirling/latest/logs:/logs:rw
environment:
DOCKER_ENABLE_SECURITY: "false"
SECURITY_ENABLELOGIN: "false"

View File

@@ -1,7 +1,7 @@
services:
stirling-pdf:
container_name: Stirling-PDF
image: frooodle/s-pdf:latest
image: stirlingtools/stirling-pdf:latest
deploy:
resources:
limits:
@@ -14,9 +14,9 @@ services:
ports:
- "8080:8080"
volumes:
- /stirling/latest/data:/usr/share/tessdata:rw
- /stirling/latest/config:/configs:rw
- /stirling/latest/logs:/logs:rw
- ./stirling/latest/data:/usr/share/tessdata:rw
- ./stirling/latest/config:/configs:rw
- ./stirling/latest/logs:/logs:rw
environment:
DOCKER_ENABLE_SECURITY: "false"
SECURITY_ENABLELOGIN: "false"

View File

@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@@ -3,6 +3,11 @@ ignore = [
'language.direction',
]
[az_AZ]
ignore = [
'language.direction',
]
[bg_BG]
ignore = [
'language.direction',
@@ -13,13 +18,11 @@ ignore = [
'PDFToText.tags',
'adminUserSettings.admin',
'language.direction',
'survey.button',
'watermark.type.1',
]
[cs_CZ]
ignore = [
'info',
'language.direction',
'pipeline.header',
'text',
@@ -39,12 +42,19 @@ ignore = [
'addPageNumbers.selectText.3',
'alphabet',
'certSign.name',
'fileChooser.dragAndDrop',
'home.pipeline.title',
'language.direction',
'legal.impressum',
'licenses.version',
'pipeline.title',
'pipelineOptions.pipelineHeader',
'pro',
'sponsor',
'text',
'validateSignature.cert.bits',
'validateSignature.cert.version',
'validateSignature.status',
'watermark.type.1',
]
@@ -79,7 +89,6 @@ ignore = [
'alphabet',
'compare.document.1',
'compare.document.2',
'info',
'language.direction',
'licenses.license',
'licenses.module',
@@ -87,8 +96,6 @@ ignore = [
'licenses.version',
'pdfOrganiser.mode',
'pipeline.title',
'pipelineOptions.pipelineHeader',
'sponsor',
'watermark.type.2',
]
@@ -105,11 +112,8 @@ ignore = [
[hr_HR]
ignore = [
'PDFToBook.selectText.1',
'font',
'home.pipeline.title',
'info',
'language.direction',
'pdfOrganiser.tags',
'showJS.tags',
]
@@ -125,7 +129,6 @@ ignore = [
[it_IT]
ignore = [
'font',
'language.direction',
'no',
'password',
@@ -148,18 +151,10 @@ ignore = [
[nl_NL]
ignore = [
'HTMLToPDF.print',
'adjustContrast.contrast',
'compare.document.1',
'compare.document.2',
'error',
'getPdfInfo.downloadJson',
'help',
'info',
'language.direction',
'navbar.allTools',
'printFile.submit',
'showJS.downloadJS',
'sponsor',
]
@@ -181,7 +176,6 @@ ignore = [
[pt_BR]
ignore = [
'changeMetadata.trapped',
'language.direction',
'pipelineOptions.pipelineHeader',
]
@@ -227,7 +221,6 @@ ignore = [
[th_TH]
ignore = [
'language.direction',
'pipeline.title',
'pipelineOptions.pipelineHeader',
'showJS.tags',
]

View File

@@ -1,37 +1,11 @@
#!/bin/bash
# Update the user and group IDs as per environment variables
if [ ! -z "$PUID" ] && [ "$PUID" != "$(id -u stirlingpdfuser)" ]; then
usermod -o -u "$PUID" stirlingpdfuser || true
fi
if [ ! -z "$PGID" ] && [ "$PGID" != "$(getent group stirlingpdfgroup | cut -d: -f3)" ]; then
groupmod -o -g "$PGID" stirlingpdfgroup || true
fi
umask "$UMASK" || true
if [[ "$INSTALL_BOOK_AND_ADVANCED_HTML_OPS" == "true" && "$FAT_DOCKER" != "true" ]]; then
echo "issue with calibre in current version, feature currently disabled on Stirling-PDF"
#apk add --no-cache calibre@testing
fi
if [[ "$FAT_DOCKER" != "true" ]]; then
/scripts/download-security-jar.sh
/scripts/download-security-jar.sh
fi
if [[ -n "$LANGS" ]]; then
/scripts/installFonts.sh $LANGS
/scripts/installFonts.sh $LANGS
fi
echo "Setting permissions and ownership for necessary directories..."
# Attempt to change ownership of directories and files
if chown -R stirlingpdfuser:stirlingpdfgroup $HOME /logs /scripts /usr/share/fonts/opentype/noto /configs /customFiles /pipeline /app.jar; then
chmod -R 755 /logs /scripts /usr/share/fonts/opentype/noto /configs /customFiles /pipeline /app.jar || true
# If chown succeeds, execute the command as stirlingpdfuser
exec su-exec stirlingpdfuser "$@"
else
# If chown fails, execute the command without changing the user context
echo "[WARN] Chown failed, running as host user"
exec "$@"
fi
exec "$@"

View File

@@ -1,31 +1,39 @@
#!/bin/bash
# Copy the original tesseract-ocr files to the volume directory without overwriting existing files
echo "Copying original files without overwriting existing files"
mkdir -p /usr/share/tessdata
cp -rn /usr/share/tessdata-original/* /usr/share/tessdata
cp -rn /usr/share/tessdata-original/* /usr/share/tessdata 2>/dev/null || true
# Copy additional tessdata if available
if [ -d /usr/share/tesseract-ocr/4.00/tessdata ]; then
cp -r /usr/share/tesseract-ocr/4.00/tessdata/* /usr/share/tessdata || true;
cp -rn /usr/share/tesseract-ocr/4.00/tessdata/* /usr/share/tessdata 2>/dev/null || true
fi
if [ -d /usr/share/tesseract-ocr/5/tessdata ]; then
cp -r /usr/share/tesseract-ocr/5/tessdata/* /usr/share/tessdata || true;
cp -rn /usr/share/tesseract-ocr/5/tessdata/* /usr/share/tessdata 2>/dev/null || true
fi
# Check if TESSERACT_LANGS environment variable is set and is not empty
if [[ -n "$TESSERACT_LANGS" ]]; then
# Convert comma-separated values to a space-separated list
LANGS=$(echo $TESSERACT_LANGS | tr ',' ' ')
pattern='^[a-zA-Z]{2,4}(_[a-zA-Z]{2,4})?$'
# Install each language pack
for LANG in $LANGS; do
if [[ $LANG =~ $pattern ]]; then
apk add --no-cache "tesseract-ocr-data-$LANG"
else
echo "Skipping invalid language code"
fi
done
# Convert comma-separated values to a space-separated list
TES_LANGS=$(echo $TESSERACT_LANGS | tr ',' ' ')
pattern='^[a-zA-Z]{2,4}(_[a-zA-Z]{2,4})?$'
# Log available languages
echo "Currently installed languages:"
tesseract --list-langs
echo "Requested additional languages: $TES_LANGS"
# Instead of apk add, download language files from a known source
for LANG in $TES_LANGS; do
if [[ $LANG =~ $pattern ]]; then
# Download to user-writable directory
wget -P /usr/share/tessdata/ "https://github.com/tesseract-ocr/tessdata/raw/main/${LANG}.traineddata" || \
echo "Failed to download language pack for ${LANG}"
else
echo "Skipping invalid language code"
fi
done
fi
/scripts/init-without-ocr.sh "$@"

View File

@@ -1,67 +1,156 @@
#!/bin/bash
LANGS=$1
FONT_DIR="$HOME/.local/share/fonts"
TEMP_DIR=$(mktemp -d)
# Function to install a font package
install_font() {
echo "Installing font package: $1"
if ! apk add "$1" --no-cache; then
echo "Failed to install $1"
fi
# Create fonts directory if it doesn't exist
mkdir -p "$FONT_DIR"
# Function to get latest GitHub release
get_latest_release() {
local repo=$1
local api_url="https://api.github.com/repos/$repo/releases/latest"
curl --silent "$api_url" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/'
}
# Install common fonts used across many languages
#common_fonts=(
# font-terminus
# font-dejavu
# font-noto
# font-noto-cjk
# font-awesome
# font-noto-extra
#)
#
#for font in "${common_fonts[@]}"; do
# install_font $font
#done
# Function to download and install a font
install_font() {
local font_name=$1
echo "Installing font package: $font_name"
# Map font package names to actual font URLs and installation methods
case $font_name in
"font-dejavu")
local version=$(get_latest_release "dejavu-fonts/dejavu-fonts")
version=${version#version_} # Remove 'version_' prefix
local url="https://github.com/dejavu-fonts/dejavu-fonts/releases/download/version_${version}/dejavu-fonts-ttf-${version}.tar.bz2"
wget -q "$url" -P "$TEMP_DIR" && \
tar xjf "$TEMP_DIR/dejavu-fonts-ttf-${version}.tar.bz2" -C "$TEMP_DIR" && \
find "$TEMP_DIR" -name "*.ttf" -exec cp {} "$FONT_DIR/" \;
;;
"font-noto")
# Base Noto Sans and Serif
wget -q "https://noto-website-2.storage.googleapis.com/pkgs/NotoSans-hinted.zip" -P "$TEMP_DIR" && \
wget -q "https://noto-website-2.storage.googleapis.com/pkgs/NotoSerif-hinted.zip" -P "$TEMP_DIR" && \
unzip -q "$TEMP_DIR/NotoSans-hinted.zip" -d "$TEMP_DIR/noto-sans" && \
unzip -q "$TEMP_DIR/NotoSerif-hinted.zip" -d "$TEMP_DIR/noto-serif" && \
cp "$TEMP_DIR/noto-sans"/*.ttf "$FONT_DIR/" && \
cp "$TEMP_DIR/noto-serif"/*.ttf "$FONT_DIR/"
;;
"font-noto-cjk")
# Noto CJK
wget -q "https://github.com/notofonts/noto-cjk/raw/main/Sans/OTF/Japanese/NotoSansCJKjp-Regular.otf" -P "$FONT_DIR"
wget -q "https://github.com/notofonts/noto-cjk/raw/main/Sans/OTF/Korean/NotoSansCJKkr-Regular.otf" -P "$FONT_DIR"
wget -q "https://github.com/notofonts/noto-cjk/raw/main/Sans/OTF/SimplifiedChinese/NotoSansCJKsc-Regular.otf" -P "$FONT_DIR"
wget -q "https://github.com/notofonts/noto-cjk/raw/main/Sans/OTF/TraditionalChinese/NotoSansCJKtc-Regular.otf" -P "$FONT_DIR"
;;
"font-noto-arabic")
wget -q "https://github.com/notofonts/noto-fonts/raw/main/hinted/ttf/NotoNaskhArabic/NotoNaskhArabic-Regular.ttf" -P "$FONT_DIR"
wget -q "https://github.com/notofonts/noto-fonts/raw/main/hinted/ttf/NotoKufiArabic/NotoKufiArabic-Regular.ttf" -P "$FONT_DIR"
;;
"font-noto-devanagari")
wget -q "https://github.com/notofonts/noto-fonts/raw/main/hinted/ttf/NotoSansDevanagari/NotoSansDevanagari-Regular.ttf" -P "$FONT_DIR"
wget -q "https://github.com/notofonts/noto-fonts/raw/main/hinted/ttf/NotoSerifDevanagari/NotoSerifDevanagari-Regular.ttf" -P "$FONT_DIR"
;;
"font-noto-thai")
wget -q "https://github.com/notofonts/noto-fonts/raw/main/hinted/ttf/NotoSansThai/NotoSansThai-Regular.ttf" -P "$FONT_DIR"
wget -q "https://github.com/notofonts/noto-fonts/raw/main/hinted/ttf/NotoSerifThai/NotoSerifThai-Regular.ttf" -P "$FONT_DIR"
;;
"font-noto-hebrew")
wget -q "https://github.com/notofonts/noto-fonts/raw/main/hinted/ttf/NotoSansHebrew/NotoSansHebrew-Regular.ttf" -P "$FONT_DIR"
;;
"font-awesome")
local version=$(get_latest_release "FortAwesome/Font-Awesome")
wget -q "https://use.fontawesome.com/releases/v${version}/fontawesome-free-${version}-desktop.zip" -P "$TEMP_DIR" && \
unzip -q "$TEMP_DIR/fontawesome-free-${version}-desktop.zip" -d "$TEMP_DIR" && \
cp "$TEMP_DIR/fontawesome-free-${version}-desktop/otfs"/*.otf "$FONT_DIR/"
;;
"font-source-code-pro")
local version=$(get_latest_release "adobe-fonts/source-code-pro")
wget -q "https://github.com/adobe-fonts/source-code-pro/releases/download/${version}/TTF-source-code-pro-${version}.zip" -P "$TEMP_DIR" && \
unzip -q "$TEMP_DIR/TTF-source-code-pro-${version}.zip" -d "$TEMP_DIR/source-code-pro" && \
cp "$TEMP_DIR/source-code-pro"/*.ttf "$FONT_DIR/"
;;
"font-vollkorn")
wget -q "https://github.com/FAlthausen/Vollkorn-Typeface/raw/main/fonts/TTF/Vollkorn-Regular.ttf" -P "$FONT_DIR"
;;
"font-liberation")
wget -q "https://github.com/liberationfonts/liberation-fonts/files/7261482/liberation-fonts-ttf-2.1.5.tar.gz" -P "$TEMP_DIR" && \
tar xzf "$TEMP_DIR/liberation-fonts-ttf-2.1.5.tar.gz" -C "$TEMP_DIR" && \
cp "$TEMP_DIR/liberation-fonts-ttf-2.1.5"/*.ttf "$FONT_DIR/"
;;
esac
echo "Completed installation attempt for $font_name"
}
# Map languages to specific font packages
# Enhanced language-specific font mappings
declare -A language_fonts=(
["ar_AR"]="font-noto-arabic"
["zh_CN"]="font-isas-misc"
["zh_TW"]="font-isas-misc"
["ja_JP"]="font-noto font-noto-thai font-noto-tibetan font-ipa font-sony-misc font-jis-misc"
["ru_RU"]="font-vollkorn font-misc-cyrillic font-mutt-misc font-screen-cyrillic font-winitzki-cyrillic font-cronyx-cyrillic"
["sr_LATN_RS"]="font-vollkorn font-misc-cyrillic font-mutt-misc font-screen-cyrillic font-winitzki-cyrillic font-cronyx-cyrillic"
["uk_UA"]="font-vollkorn font-misc-cyrillic font-mutt-misc font-screen-cyrillic font-winitzki-cyrillic font-cronyx-cyrillic"
["ko_KR"]="font-noto font-noto-thai font-noto-tibetan"
["zh_CN"]="font-noto-cjk"
["zh_TW"]="font-noto-cjk"
["ja_JP"]="font-noto font-noto-cjk"
["ru_RU"]="font-noto font-liberation font-vollkorn"
["sr_LATN_RS"]="font-noto font-liberation"
["uk_UA"]="font-noto font-liberation"
["ko_KR"]="font-noto font-noto-cjk"
["el_GR"]="font-noto"
["hi_IN"]="font-noto-devanagari"
["bg_BG"]="font-vollkorn font-misc-cyrillic"
["GENERAL"]="font-terminus font-dejavu font-noto font-noto-cjk font-awesome font-noto-extra"
["bg_BG"]="font-noto font-liberation"
["th_TH"]="font-noto-thai"
["he_IL"]="font-noto-hebrew"
["GENERAL"]="font-noto font-dejavu font-liberation font-source-code-pro font-awesome"
)
# Install fonts for other languages which generally do not need special packages beyond 'font-noto'
other_langs=("en_GB" "en_US" "de_DE" "fr_FR" "es_ES" "ca_CA" "it_IT" "pt_BR" "nl_NL" "sv_SE" "pl_PL" "ro_RO" "hu_HU" "tr_TR" "id_ID" "eu_ES")
# Install fonts based on specified languages
if [[ $LANGS == "ALL" ]]; then
# Install all fonts from the language_fonts map
declare -A installed_fonts
for fonts in "${language_fonts[@]}"; do
for font in $fonts; do
install_font $font
if [[ -z "${installed_fonts[$font]}" ]]; then
install_font "$font"
installed_fonts[$font]=1
fi
done
done
else
# Split comma-separated languages and install necessary fonts
declare -A installed_fonts
IFS=',' read -ra LANG_CODES <<< "$LANGS"
for code in "${LANG_CODES[@]}"; do
if [[ " ${other_langs[@]} " =~ " ${code} " ]]; then
install_font font-noto
else
fonts_to_install=${language_fonts[$code]}
if [ ! -z "$fonts_to_install" ]; then
for font in $fonts_to_install; do
install_font $font
done
fi
fonts_to_install=${language_fonts[$code]}
if [ ! -z "$fonts_to_install" ]; then
for font in $fonts_to_install; do
if [[ -z "${installed_fonts[$font]}" ]]; then
install_font "$font"
installed_fonts[$font]=1
fi
done
fi
done
fi
# Cleanup
rm -rf "$TEMP_DIR"
# Update font cache
if command -v fc-cache >/dev/null; then
fc-cache -f "$FONT_DIR"
echo "Font cache updated"
else
echo "Warning: fc-cache not found. You may need to manually update your font cache"
fi
echo "Font installation completed. Fonts installed in: $FONT_DIR"

View File

@@ -1,8 +1,8 @@
#!/bin/bash
translation_key="pdfToPDFA.credit"
old_value="OCRmyPDF"
new_value="ghostscript"
old_value="qpdf"
new_value="liibreoffice"
for file in ../src/main/resources/messages_*.properties; do
sed -i "/^$translation_key=/s/$old_value/$new_value/" "$file"

View File

@@ -3,13 +3,14 @@ package stirling.software.SPDF.EE;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.ApplicationProperties;
@Configuration
@Lazy
@Order(Ordered.HIGHEST_PRECEDENCE)
@Slf4j
public class EEAppConfig {

View File

@@ -25,9 +25,10 @@ public class LicenseKeyChecker {
KeygenLicenseVerifier licenseService, ApplicationProperties applicationProperties) {
this.licenseService = licenseService;
this.applicationProperties = applicationProperties;
this.checkLicense();
}
@Scheduled(fixedRate = 604800000, initialDelay = 1000) // 7 days in milliseconds
@Scheduled(initialDelay = 604800000, fixedRate = 604800000) // 7 days in milliseconds
public void checkLicensePeriodically() {
checkLicense();
}

View File

@@ -117,7 +117,6 @@ public class EndpointConfiguration {
addEndpointToGroup("Convert", "img-to-pdf");
addEndpointToGroup("Convert", "pdf-to-pdfa");
addEndpointToGroup("Convert", "file-to-pdf");
addEndpointToGroup("Convert", "xlsx-to-pdf");
addEndpointToGroup("Convert", "pdf-to-word");
addEndpointToGroup("Convert", "pdf-to-presentation");
addEndpointToGroup("Convert", "pdf-to-text");
@@ -163,7 +162,6 @@ public class EndpointConfiguration {
addEndpointToGroup("CLI", "repair");
addEndpointToGroup("CLI", "pdf-to-pdfa");
addEndpointToGroup("CLI", "file-to-pdf");
addEndpointToGroup("CLI", "xlsx-to-pdf");
addEndpointToGroup("CLI", "pdf-to-word");
addEndpointToGroup("CLI", "pdf-to-presentation");
addEndpointToGroup("CLI", "pdf-to-html");
@@ -184,24 +182,28 @@ public class EndpointConfiguration {
addEndpointToGroup("Python", "html-to-pdf");
addEndpointToGroup("Python", "url-to-pdf");
addEndpointToGroup("Python", "pdf-to-img");
addEndpointToGroup("Python", "file-to-pdf");
// openCV
addEndpointToGroup("OpenCV", "extract-image-scans");
// LibreOffice
addEndpointToGroup("LibreOffice", "repair");
addEndpointToGroup("qpdf", "repair");
addEndpointToGroup("LibreOffice", "file-to-pdf");
addEndpointToGroup("LibreOffice", "xlsx-to-pdf");
addEndpointToGroup("LibreOffice", "pdf-to-word");
addEndpointToGroup("LibreOffice", "pdf-to-presentation");
addEndpointToGroup("LibreOffice", "pdf-to-rtf");
addEndpointToGroup("LibreOffice", "pdf-to-html");
addEndpointToGroup("LibreOffice", "pdf-to-xml");
// OCRmyPDF
addEndpointToGroup("OCRmyPDF", "compress-pdf");
addEndpointToGroup("OCRmyPDF", "pdf-to-pdfa");
addEndpointToGroup("OCRmyPDF", "ocr-pdf");
// Unoconv
addEndpointToGroup("Unoconv", "file-to-pdf");
// qpdf
addEndpointToGroup("qpdf", "compress-pdf");
addEndpointToGroup("qpdf", "pdf-to-pdfa");
addEndpointToGroup("tesseract", "ocr-pdf");
// Java
addEndpointToGroup("Java", "merge-pdfs");
@@ -247,9 +249,10 @@ public class EndpointConfiguration {
addEndpointToGroup("Javascript", "compare");
addEndpointToGroup("Javascript", "adjust-contrast");
// Ghostscript dependent endpoints
addEndpointToGroup("Ghostscript", "compress-pdf");
addEndpointToGroup("Ghostscript", "pdf-to-pdfa");
// qpdf dependent endpoints
addEndpointToGroup("qpdf", "compress-pdf");
addEndpointToGroup("qpdf", "pdf-to-pdfa");
addEndpointToGroup("qpdf", "repair");
// Weasyprint dependent endpoints
addEndpointToGroup("Weasyprint", "html-to-pdf");

View File

@@ -37,11 +37,12 @@ public class ExternalAppDepConfig {
private final Map<String, List<String>> commandToGroupMapping =
new HashMap<>() {
{
put("gs", List.of("Ghostscript"));
put("soffice", List.of("LibreOffice"));
put("ocrmypdf", List.of("OCRmyPDF"));
put("weasyprint", List.of("Weasyprint"));
put("pdftohtml", List.of("Pdftohtml"));
put("unoconv", List.of("Unoconv"));
put("qpdf", List.of("qpdf"));
put("tesseract", List.of("tesseract"));
}
};
@@ -96,11 +97,12 @@ public class ExternalAppDepConfig {
public void checkDependencies() {
// Check core dependencies
checkDependencyAndDisableGroup("gs");
checkDependencyAndDisableGroup("tesseract");
checkDependencyAndDisableGroup("soffice");
checkDependencyAndDisableGroup("ocrmypdf");
checkDependencyAndDisableGroup("qpdf");
checkDependencyAndDisableGroup("weasyprint");
checkDependencyAndDisableGroup("pdftohtml");
checkDependencyAndDisableGroup("unoconv");
// Special handling for Python/OpenCV dependencies
boolean pythonAvailable = isCommandAvailable("python3") || isCommandAvailable("python");

View File

@@ -30,6 +30,7 @@ public class InitialSecuritySetup {
initializeAdminUser();
} else {
databaseBackupHelper.exportDatabase();
userService.migrateOauth2ToSSO();
}
initializeInternalApiUser();
}

View File

@@ -3,14 +3,16 @@ package stirling.software.SPDF.config.security;
import java.security.cert.X509Certificate;
import java.util.*;
import org.opensaml.saml.saml2.core.AuthnRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.io.Resource;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
@@ -32,7 +34,8 @@ import org.springframework.security.saml2.provider.service.authentication.OpenSa
import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
import org.springframework.security.saml2.provider.service.web.authentication.Saml2WebSsoAuthenticationFilter;
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
@@ -41,6 +44,7 @@ import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
import org.springframework.security.web.savedrequest.NullRequestCache;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationFailureHandler;
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationSuccessHandler;
@@ -64,6 +68,7 @@ import stirling.software.SPDF.repository.JPATokenRepositoryImpl;
@EnableWebSecurity
@EnableMethodSecurity
@Slf4j
@DependsOn("runningEE")
public class SecurityConfiguration {
@Autowired private CustomUserDetailsService userDetailsService;
@@ -79,6 +84,10 @@ public class SecurityConfiguration {
@Qualifier("loginEnabled")
public boolean loginEnabledValue;
@Autowired
@Qualifier("runningEE")
public boolean runningEE;
@Autowired ApplicationProperties applicationProperties;
@Autowired private UserAuthenticationFilter userAuthenticationFilter;
@@ -90,13 +99,14 @@ public class SecurityConfiguration {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
if (applicationProperties.getSecurity().getCsrfDisabled()) {
http.csrf(csrf -> csrf.disable());
}
if (loginEnabledValue) {
http.addFilterBefore(
userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
if (applicationProperties.getSecurity().getCsrfDisabled()) {
http.csrf(csrf -> csrf.disable());
} else {
if (!applicationProperties.getSecurity().getCsrfDisabled()) {
CookieCsrfTokenRepository cookieRepo =
CookieCsrfTokenRepository.withHttpOnlyFalse();
CsrfTokenRequestAttributeHandler requestHandler =
@@ -156,10 +166,14 @@ public class SecurityConfiguration {
http.rememberMe(
rememberMeConfigurer ->
rememberMeConfigurer // Use the configurator directly
.key("uniqueAndSecret")
.tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(1209600) // 2 weeks
);
.tokenValiditySeconds(14 * 24 * 60 * 60) // 14 days
.userDetailsService(
userDetailsService) // Your existing UserDetailsService
.useSecureCookie(true) // Enable secure cookie
.rememberMeParameter("remember-me") // Form parameter name
.rememberMeCookieName("remember-me") // Cookie name
.alwaysRemember(false));
http.authorizeHttpRequests(
authz ->
authz.requestMatchers(
@@ -241,12 +255,22 @@ public class SecurityConfiguration {
}
// Handle SAML
if (applicationProperties.getSecurity().isSaml2Activ()
&& applicationProperties.getSystem().getEnableAlphaFunctionality()) {
http.authenticationProvider(samlAuthenticationProvider());
http.saml2Login(
saml2 ->
if (applicationProperties.getSecurity().isSaml2Activ()) { // && runningEE
// Configure the authentication provider
OpenSaml4AuthenticationProvider authenticationProvider =
new OpenSaml4AuthenticationProvider();
authenticationProvider.setResponseAuthenticationConverter(
new CustomSaml2ResponseAuthenticationConverter(userService));
http.authenticationProvider(authenticationProvider)
.saml2Login(
saml2 -> {
try {
saml2.loginPage("/saml2")
.relyingPartyRegistrationRepository(
relyingPartyRegistrations())
.authenticationManager(
new ProviderManager(authenticationProvider))
.successHandler(
new CustomSaml2AuthenticationSuccessHandler(
loginAttemptService,
@@ -254,14 +278,18 @@ public class SecurityConfiguration {
userService))
.failureHandler(
new CustomSaml2AuthenticationFailureHandler())
.permitAll())
.addFilterBefore(
userAuthenticationFilter, Saml2WebSsoAuthenticationFilter.class);
.authenticationRequestResolver(
authenticationRequestResolver(
relyingPartyRegistrations()));
} catch (Exception e) {
log.error("Error configuring SAML2 login", e);
throw new RuntimeException(e);
}
});
}
} else {
if (applicationProperties.getSecurity().getCsrfDisabled()) {
http.csrf(csrf -> csrf.disable());
} else {
if (!applicationProperties.getSecurity().getCsrfDisabled()) {
CookieCsrfTokenRepository cookieRepo =
CookieCsrfTokenRepository.withHttpOnlyFalse();
CsrfTokenRequestAttributeHandler requestHandler =
@@ -278,20 +306,6 @@ public class SecurityConfiguration {
return http.build();
}
@Bean
@ConditionalOnProperty(
name = "security.saml2.enabled",
havingValue = "true",
matchIfMissing = false)
public AuthenticationProvider samlAuthenticationProvider() {
OpenSaml4AuthenticationProvider authenticationProvider =
new OpenSaml4AuthenticationProvider();
authenticationProvider.setResponseAuthenticationConverter(
new CustomSaml2ResponseAuthenticationConverter(userService));
return authenticationProvider;
}
// Client Registration Repository for OAUTH2 OIDC Login
@Bean
@ConditionalOnProperty(
value = "security.oauth2.enabled",
@@ -428,11 +442,12 @@ public class SecurityConfiguration {
havingValue = "true",
matchIfMissing = false)
public RelyingPartyRegistrationRepository relyingPartyRegistrations() throws Exception {
SAML2 samlConf = applicationProperties.getSecurity().getSaml2();
Resource privateKeyResource = samlConf.getPrivateKey();
X509Certificate idpCert = CertificateUtils.readCertificate(samlConf.getidpCert());
Saml2X509Credential verificationCredential = Saml2X509Credential.verification(idpCert);
Resource privateKeyResource = samlConf.getPrivateKey();
Resource certificateResource = samlConf.getSpCert();
Saml2X509Credential signingCredential =
@@ -441,26 +456,97 @@ public class SecurityConfiguration {
CertificateUtils.readCertificate(certificateResource),
Saml2X509CredentialType.SIGNING);
X509Certificate idpCert = CertificateUtils.readCertificate(samlConf.getidpCert());
Saml2X509Credential verificationCredential = Saml2X509Credential.verification(idpCert);
RelyingPartyRegistration rp =
RelyingPartyRegistration.withRegistrationId(samlConf.getRegistrationId())
.signingX509Credentials((c) -> c.add(signingCredential))
.assertingPartyDetails(
(details) ->
details.entityId(samlConf.getIdpIssuer())
.signingX509Credentials(c -> c.add(signingCredential))
.assertingPartyMetadata(
metadata ->
metadata.entityId(samlConf.getIdpIssuer())
.singleSignOnServiceLocation(
samlConf.getIdpSingleLoginUrl())
.verificationX509Credentials(
(c) -> c.add(verificationCredential))
c -> c.add(verificationCredential))
.singleSignOnServiceBinding(
Saml2MessageBinding.POST)
.wantAuthnRequestsSigned(true))
.build();
return new InMemoryRelyingPartyRegistrationRepository(rp);
}
@Bean
@ConditionalOnProperty(
name = "security.saml2.enabled",
havingValue = "true",
matchIfMissing = false)
public OpenSaml4AuthenticationRequestResolver authenticationRequestResolver(
RelyingPartyRegistrationRepository relyingPartyRegistrationRepository) {
OpenSaml4AuthenticationRequestResolver resolver =
new OpenSaml4AuthenticationRequestResolver(relyingPartyRegistrationRepository);
resolver.setAuthnRequestCustomizer(
customizer -> {
log.debug("Customizing SAML Authentication request");
AuthnRequest authnRequest = customizer.getAuthnRequest();
log.debug("AuthnRequest ID: {}", authnRequest.getID());
if (authnRequest.getID() == null) {
authnRequest.setID("ARQ" + UUID.randomUUID().toString());
}
log.debug("AuthnRequest new ID after set: {}", authnRequest.getID());
log.debug("AuthnRequest IssueInstant: {}", authnRequest.getIssueInstant());
log.debug(
"AuthnRequest Issuer: {}",
authnRequest.getIssuer() != null
? authnRequest.getIssuer().getValue()
: "null");
HttpServletRequest request = customizer.getRequest();
// Log HTTP request details
log.debug("HTTP Request Method: {}", request.getMethod());
log.debug("Request URI: {}", request.getRequestURI());
log.debug("Request URL: {}", request.getRequestURL().toString());
log.debug("Query String: {}", request.getQueryString());
log.debug("Remote Address: {}", request.getRemoteAddr());
// Log headers
Collections.list(request.getHeaderNames())
.forEach(
headerName -> {
log.debug(
"Header - {}: {}",
headerName,
request.getHeader(headerName));
});
// Log SAML specific parameters
log.debug("SAML Request Parameters:");
log.debug("SAMLRequest: {}", request.getParameter("SAMLRequest"));
log.debug("RelayState: {}", request.getParameter("RelayState"));
// Log session debugrmation if exists
if (request.getSession(false) != null) {
log.debug("Session ID: {}", request.getSession().getId());
}
// Log any assertions consumer service details if present
if (authnRequest.getAssertionConsumerServiceURL() != null) {
log.debug(
"AssertionConsumerServiceURL: {}",
authnRequest.getAssertionConsumerServiceURL());
}
// Log NameID policy if present
if (authnRequest.getNameIDPolicy() != null) {
log.debug(
"NameIDPolicy Format: {}",
authnRequest.getNameIDPolicy().getFormat());
}
});
return resolver;
}
public DaoAuthenticationProvider daoAuthenticationProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService);

View File

@@ -18,11 +18,14 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.interfaces.DatabaseBackupInterface;
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal;
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
import stirling.software.SPDF.controller.api.pipeline.UserServiceInterface;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.AuthenticationType;
import stirling.software.SPDF.model.Authority;
import stirling.software.SPDF.model.Role;
@@ -31,6 +34,7 @@ import stirling.software.SPDF.repository.AuthorityRepository;
import stirling.software.SPDF.repository.UserRepository;
@Service
@Slf4j
public class UserService implements UserServiceInterface {
@Autowired private UserRepository userRepository;
@@ -45,8 +49,21 @@ public class UserService implements UserServiceInterface {
@Autowired DatabaseBackupInterface databaseBackupHelper;
@Autowired ApplicationProperties applicationProperties;
@Transactional
public void migrateOauth2ToSSO() {
userRepository
.findByAuthenticationTypeIgnoreCase("OAUTH2")
.forEach(
user -> {
user.setAuthenticationType(AuthenticationType.SSO);
userRepository.save(user);
});
}
// Handle OAUTH2 login and user auto creation.
public boolean processOAuth2PostLogin(String username, boolean autoCreateUser)
public boolean processSSOPostLogin(String username, boolean autoCreateUser)
throws IllegalArgumentException, IOException {
if (!isUsernameValid(username)) {
return false;
@@ -56,7 +73,7 @@ public class UserService implements UserServiceInterface {
return true;
}
if (autoCreateUser) {
saveUser(username, AuthenticationType.OAUTH2);
saveUser(username, AuthenticationType.SSO);
return true;
}
return false;
@@ -299,7 +316,13 @@ public class UserService implements UserServiceInterface {
boolean isValidEmail =
username.matches(
"^(?=.{1,64}@)[A-Za-z0-9]+(\\.[A-Za-z0-9_+.-]+)*@[^-][A-Za-z0-9-]+(\\.[A-Za-z0-9-]+)*(\\.[A-Za-z]{2,})$");
return isValidSimpleUsername || isValidEmail;
List<String> notAllowedUserList = new ArrayList<>();
notAllowedUserList.add("ALL_USERS".toLowerCase());
boolean notAllowedUser = notAllowedUserList.contains(username.toLowerCase());
return (isValidSimpleUsername || isValidEmail) && !notAllowedUser;
}
private String getInvalidUsernameMessage() {
@@ -354,6 +377,14 @@ public class UserService implements UserServiceInterface {
if (principal instanceof UserDetails) {
return ((UserDetails) principal).getUsername();
} else if (principal instanceof OAuth2User) {
return ((OAuth2User) principal)
.getAttribute(
applicationProperties.getSecurity().getOauth2().getUseAsUsername());
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal) {
return ((CustomSaml2AuthenticatedPrincipal) principal).getName();
} else if (principal instanceof String) {
return (String) principal;
} else {
return principal.toString();
}

View File

@@ -34,6 +34,12 @@ public class DatabaseBackupHelper implements DatabaseBackupInterface {
@Value("${spring.datasource.url}")
private String url;
@Value("${spring.datasource.username}")
private String databaseUsername;
@Value("${spring.datasource.password}")
private String databasePassword;
private Path backupPath = Paths.get("configs/db/backup/");
@Override
@@ -134,7 +140,8 @@ public class DatabaseBackupHelper implements DatabaseBackupInterface {
this.getBackupFilePath("backup_" + dateNow.format(myFormatObj) + ".sql");
String query = "SCRIPT SIMPLE COLUMNS DROP to ?;";
try (Connection conn = DriverManager.getConnection(url, "sa", "");
try (Connection conn =
DriverManager.getConnection(url, databaseUsername, databasePassword);
PreparedStatement stmt = conn.prepareStatement(query)) {
stmt.setString(1, insertOutputFilePath.toString());
stmt.execute();
@@ -147,7 +154,8 @@ public class DatabaseBackupHelper implements DatabaseBackupInterface {
// Retrieves the H2 database version.
public String getH2Version() {
String version = "Unknown";
try (Connection conn = DriverManager.getConnection(url, "sa", "")) {
try (Connection conn =
DriverManager.getConnection(url, databaseUsername, databasePassword)) {
try (Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT H2VERSION() AS version")) {
if (rs.next()) {
@@ -189,7 +197,8 @@ public class DatabaseBackupHelper implements DatabaseBackupInterface {
private boolean executeDatabaseScript(Path scriptPath) {
String query = "RUNSCRIPT from ?;";
try (Connection conn = DriverManager.getConnection(url, "sa", "");
try (Connection conn =
DriverManager.getConnection(url, databaseUsername, databasePassword);
PreparedStatement stmt = conn.prepareStatement(query)) {
stmt.setString(1, scriptPath.toString());
stmt.execute();

View File

@@ -82,8 +82,7 @@ public class CustomOAuth2AuthenticationSuccessHandler
}
if (userService.usernameExistsIgnoreCase(username)
&& userService.hasPassword(username)
&& !userService.isAuthenticationTypeByUsername(
username, AuthenticationType.OAUTH2)
&& !userService.isAuthenticationTypeByUsername(username, AuthenticationType.SSO)
&& oAuth.getAutoCreateUser()) {
response.sendRedirect(contextPath + "/logout?oauth2AuthenticationErrorWeb=true");
return;
@@ -95,7 +94,7 @@ public class CustomOAuth2AuthenticationSuccessHandler
return;
}
if (principal instanceof OAuth2User) {
userService.processOAuth2PostLogin(username, oAuth.getAutoCreateUser());
userService.processSSOPostLogin(username, oAuth.getAutoCreateUser());
}
response.sendRedirect(contextPath + "/");
return;

View File

@@ -3,12 +3,14 @@ package stirling.software.SPDF.config.security.saml2;
import java.io.ByteArrayInputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPrivateKey;
import java.security.spec.PKCS8EncodedKeySpec;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.openssl.PEMKeyPair;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import org.bouncycastle.util.io.pem.PemObject;
import org.bouncycastle.util.io.pem.PemReader;
import org.springframework.core.io.Resource;
@@ -28,15 +30,26 @@ public class CertificateUtils {
}
public static RSAPrivateKey readPrivateKey(Resource privateKeyResource) throws Exception {
try (PemReader pemReader =
new PemReader(
try (PEMParser pemParser =
new PEMParser(
new InputStreamReader(
privateKeyResource.getInputStream(), StandardCharsets.UTF_8))) {
PemObject pemObject = pemReader.readPemObject();
byte[] decodedKey = pemObject.getContent();
return (RSAPrivateKey)
KeyFactory.getInstance("RSA")
.generatePrivate(new PKCS8EncodedKeySpec(decodedKey));
Object object = pemParser.readObject();
JcaPEMKeyConverter converter = new JcaPEMKeyConverter();
if (object instanceof PEMKeyPair) {
// Handle traditional RSA private key format
PEMKeyPair keypair = (PEMKeyPair) object;
return (RSAPrivateKey) converter.getPrivateKey(keypair.getPrivateKeyInfo());
} else if (object instanceof PrivateKeyInfo) {
// Handle PKCS#8 format
return (RSAPrivateKey) converter.getPrivateKey((PrivateKeyInfo) object);
} else {
throw new IllegalArgumentException(
"Unsupported key format: "
+ (object != null ? object.getClass().getName() : "null"));
}
}
}
}

View File

@@ -12,6 +12,7 @@ import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.security.LoginAttemptService;
import stirling.software.SPDF.config.security.UserService;
import stirling.software.SPDF.model.ApplicationProperties;
@@ -20,11 +21,11 @@ import stirling.software.SPDF.model.AuthenticationType;
import stirling.software.SPDF.utils.RequestUriUtils;
@AllArgsConstructor
@Slf4j
public class CustomSaml2AuthenticationSuccessHandler
extends SavedRequestAwareAuthenticationSuccessHandler {
private LoginAttemptService loginAttemptService;
private ApplicationProperties applicationProperties;
private UserService userService;
@@ -34,10 +35,12 @@ public class CustomSaml2AuthenticationSuccessHandler
throws ServletException, IOException {
Object principal = authentication.getPrincipal();
log.debug("Starting SAML2 authentication success handling");
if (principal instanceof CustomSaml2AuthenticatedPrincipal) {
String username = ((CustomSaml2AuthenticatedPrincipal) principal).getName();
// Get the saved request
log.debug("Authenticated principal found for user: {}", username);
HttpSession session = request.getSession(false);
String contextPath = request.getContextPath();
SavedRequest savedRequest =
@@ -45,46 +48,77 @@ public class CustomSaml2AuthenticationSuccessHandler
? (SavedRequest) session.getAttribute("SPRING_SECURITY_SAVED_REQUEST")
: null;
log.debug(
"Session exists: {}, Saved request exists: {}",
session != null,
savedRequest != null);
if (savedRequest != null
&& !RequestUriUtils.isStaticResource(
contextPath, savedRequest.getRedirectUrl())) {
// Redirect to the original destination
log.debug(
"Valid saved request found, redirecting to original destination: {}",
savedRequest.getRedirectUrl());
super.onAuthenticationSuccess(request, response, authentication);
} else {
SAML2 saml2 = applicationProperties.getSecurity().getSaml2();
log.debug(
"Processing SAML2 authentication with autoCreateUser: {}",
saml2.getAutoCreateUser());
if (loginAttemptService.isBlocked(username)) {
log.debug("User {} is blocked due to too many login attempts", username);
if (session != null) {
session.removeAttribute("SPRING_SECURITY_SAVED_REQUEST");
}
throw new LockedException(
"Your account has been locked due to too many failed login attempts.");
}
if (userService.usernameExistsIgnoreCase(username)
&& userService.hasPassword(username)
&& !userService.isAuthenticationTypeByUsername(
username, AuthenticationType.OAUTH2)
&& saml2.getAutoCreateUser()) {
boolean userExists = userService.usernameExistsIgnoreCase(username);
boolean hasPassword = userExists && userService.hasPassword(username);
boolean isSSOUser =
userExists
&& userService.isAuthenticationTypeByUsername(
username, AuthenticationType.SSO);
log.debug(
"User status - Exists: {}, Has password: {}, Is SSO user: {}",
userExists,
hasPassword,
isSSOUser);
if (userExists && hasPassword && !isSSOUser && saml2.getAutoCreateUser()) {
log.debug(
"User {} exists with password but is not SSO user, redirecting to logout",
username);
response.sendRedirect(
contextPath + "/logout?oauth2AuthenticationErrorWeb=true");
return;
}
try {
if (saml2.getBlockRegistration()
&& !userService.usernameExistsIgnoreCase(username)) {
if (saml2.getBlockRegistration() && !userExists) {
log.debug("Registration blocked for new user: {}", username);
response.sendRedirect(
contextPath + "/login?erroroauth=oauth2_admin_blocked_user");
return;
}
userService.processOAuth2PostLogin(username, saml2.getAutoCreateUser());
log.debug("Processing SSO post-login for user: {}", username);
userService.processSSOPostLogin(username, saml2.getAutoCreateUser());
log.debug("Successfully processed authentication for user: {}", username);
response.sendRedirect(contextPath + "/");
return;
} catch (IllegalArgumentException e) {
log.debug(
"Invalid username detected for user: {}, redirecting to logout",
username);
response.sendRedirect(contextPath + "/logout?invalidUsername=true");
return;
}
}
} else {
log.debug("Non-SAML2 principal detected, delegating to parent handler");
super.onAuthenticationSuccess(request, response, authentication);
}
}

View File

@@ -3,8 +3,6 @@ package stirling.software.SPDF.config.security.saml2;
import java.util.*;
import org.opensaml.core.xml.XMLObject;
import org.opensaml.core.xml.schema.XSBoolean;
import org.opensaml.core.xml.schema.XSString;
import org.opensaml.saml.saml2.core.Assertion;
import org.opensaml.saml.saml2.core.Attribute;
import org.opensaml.saml.saml2.core.AttributeStatement;
@@ -30,15 +28,60 @@ public class CustomSaml2ResponseAuthenticationConverter
this.userService = userService;
}
private Map<String, List<Object>> extractAttributes(Assertion assertion) {
Map<String, List<Object>> attributes = new HashMap<>();
for (AttributeStatement attributeStatement : assertion.getAttributeStatements()) {
for (Attribute attribute : attributeStatement.getAttributes()) {
String attributeName = attribute.getName();
List<Object> values = new ArrayList<>();
for (XMLObject xmlObject : attribute.getAttributeValues()) {
// Get the text content directly
String value = xmlObject.getDOM().getTextContent();
if (value != null && !value.trim().isEmpty()) {
values.add(value);
}
}
if (!values.isEmpty()) {
// Store with both full URI and last part of the URI
attributes.put(attributeName, values);
String shortName = attributeName.substring(attributeName.lastIndexOf('/') + 1);
attributes.put(shortName, values);
}
}
}
return attributes;
}
@Override
public Saml2Authentication convert(ResponseToken responseToken) {
// Extract the assertion from the response
Assertion assertion = responseToken.getResponse().getAssertions().get(0);
Map<String, List<Object>> attributes = extractAttributes(assertion);
// Extract the NameID
String nameId = assertion.getSubject().getNameID().getValue();
// Debug log with actual values
log.debug("Extracted SAML Attributes: " + attributes);
Optional<User> userOpt = userService.findByUsernameIgnoreCase(nameId);
// Try to get username/identifier in order of preference
String userIdentifier = null;
if (hasAttribute(attributes, "username")) {
userIdentifier = getFirstAttributeValue(attributes, "username");
} else if (hasAttribute(attributes, "emailaddress")) {
userIdentifier = getFirstAttributeValue(attributes, "emailaddress");
} else if (hasAttribute(attributes, "name")) {
userIdentifier = getFirstAttributeValue(attributes, "name");
} else if (hasAttribute(attributes, "upn")) {
userIdentifier = getFirstAttributeValue(attributes, "upn");
} else if (hasAttribute(attributes, "uid")) {
userIdentifier = getFirstAttributeValue(attributes, "uid");
} else {
userIdentifier = assertion.getSubject().getNameID().getValue();
}
// Rest of your existing code...
Optional<User> userOpt = userService.findByUsernameIgnoreCase(userIdentifier);
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority("ROLE_USER");
if (userOpt.isPresent()) {
User user = userOpt.get();
@@ -48,39 +91,27 @@ public class CustomSaml2ResponseAuthenticationConverter
}
}
// Extract the SessionIndexes
List<String> sessionIndexes = new ArrayList<>();
for (AuthnStatement authnStatement : assertion.getAuthnStatements()) {
sessionIndexes.add(authnStatement.getSessionIndex());
}
// Extract the Attributes
Map<String, List<Object>> attributes = extractAttributes(assertion);
// Create the custom principal
CustomSaml2AuthenticatedPrincipal principal =
new CustomSaml2AuthenticatedPrincipal(nameId, attributes, nameId, sessionIndexes);
new CustomSaml2AuthenticatedPrincipal(
userIdentifier, attributes, userIdentifier, sessionIndexes);
// Create the Saml2Authentication
return new Saml2Authentication(
principal,
responseToken.getToken().getSaml2Response(),
Collections.singletonList(simpleGrantedAuthority));
}
private Map<String, List<Object>> extractAttributes(Assertion assertion) {
Map<String, List<Object>> attributes = new HashMap<>();
for (AttributeStatement attributeStatement : assertion.getAttributeStatements()) {
for (Attribute attribute : attributeStatement.getAttributes()) {
String attributeName = attribute.getName();
List<Object> values = new ArrayList<>();
for (XMLObject xmlObject : attribute.getAttributeValues()) {
log.info("BOOL: " + ((XSBoolean) xmlObject).getValue());
values.add(((XSString) xmlObject).getValue());
}
attributes.put(attributeName, values);
}
}
return attributes;
private boolean hasAttribute(Map<String, List<Object>> attributes, String name) {
return attributes.containsKey(name) && !attributes.get(name).isEmpty();
}
private String getFirstAttributeValue(Map<String, List<Object>> attributes, String name) {
List<Object> values = attributes.get(name);
return values != null && !values.isEmpty() ? values.get(0).toString() : null;
}
}

View File

@@ -0,0 +1,65 @@
package stirling.software.SPDF.controller.api;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import io.swagger.v3.oas.annotations.Hidden;
import jakarta.servlet.http.HttpServletResponse;
import stirling.software.SPDF.service.LanguageService;
@RestController
@RequestMapping("/js")
public class AdditionalLanguageJsController {
@Autowired private LanguageService languageService;
@Hidden
@GetMapping(value = "/additionalLanguageCode.js", produces = "application/javascript")
public void generateAdditionalLanguageJs(HttpServletResponse response) throws IOException {
List<String> supportedLanguages = languageService.getSupportedLanguages();
response.setContentType("application/javascript");
PrintWriter writer = response.getWriter();
// Erstelle das JavaScript dynamisch
writer.println("const supportedLanguages = " + toJsonArray(supportedLanguages) + ";");
// Generiere die `getDetailedLanguageCode`-Funktion
writer.println(
"""
function getDetailedLanguageCode() {
const userLanguages = navigator.languages ? navigator.languages : [navigator.language];
for (let lang of userLanguages) {
let matchedLang = supportedLanguages.find(supportedLang => supportedLang.startsWith(lang.replace('-', '_')));
if (matchedLang) {
return matchedLang;
}
}
// Fallback
return "en_GB";
}
""");
writer.flush();
}
// Hilfsfunktion zum Konvertieren der Liste in ein JSON-Array
private String toJsonArray(List<String> list) {
StringBuilder jsonArray = new StringBuilder("[");
for (int i = 0; i < list.size(); i++) {
jsonArray.append("\"").append(list.get(i)).append("\"");
if (i < list.size() - 1) {
jsonArray.append(",");
}
}
jsonArray.append("]");
return jsonArray.toString();
}
}

View File

@@ -244,8 +244,8 @@ public class UserController {
return new RedirectView("/addUsers?messageType=invalidRole", true);
}
if (authType.equalsIgnoreCase(AuthenticationType.OAUTH2.toString())) {
userService.saveUser(username, AuthenticationType.OAUTH2, role);
if (authType.equalsIgnoreCase(AuthenticationType.SSO.toString())) {
userService.saveUser(username, AuthenticationType.SSO, role);
} else {
if (password.isBlank()) {
return new RedirectView("/addUsers?messageType=invalidPassword", true);

View File

@@ -1,12 +1,13 @@
package stirling.software.SPDF.controller.api.converters;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.apache.commons.io.FileUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
@@ -37,59 +38,90 @@ public class ConvertPDFToPDFA {
@Operation(
summary = "Convert a PDF to a PDF/A",
description =
"This endpoint converts a PDF file to a PDF/A file. PDF/A is a format designed for long-term archiving of digital documents. Input:PDF Output:PDF Type:SISO")
"This endpoint converts a PDF file to a PDF/A file using LibreOffice. PDF/A is a format designed for long-term archiving of digital documents. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> pdfToPdfA(@ModelAttribute PdfToPdfARequest request)
throws Exception {
MultipartFile inputFile = request.getFileInput();
String outputFormat = request.getOutputFormat();
// Convert MultipartFile to byte[]
byte[] pdfBytes = inputFile.getBytes();
// Save the uploaded file to a temporary location
Path tempInputFile = Files.createTempFile("input_", ".pdf");
try (OutputStream outputStream = new FileOutputStream(tempInputFile.toFile())) {
outputStream.write(pdfBytes);
// Validate input file type
if (!"application/pdf".equals(inputFile.getContentType())) {
logger.error("Invalid input file type: {}", inputFile.getContentType());
throw new IllegalArgumentException("Input file must be a PDF");
}
// Prepare the output file path
Path tempOutputFile = Files.createTempFile("output_", ".pdf");
// Prepare the ghostscript command
List<String> command = new ArrayList<>();
command.add("gs");
command.add("-dPDFA=" + ("pdfa".equals(outputFormat) ? "2" : "1"));
command.add("-dNOPAUSE");
command.add("-dBATCH");
command.add("-sColorConversionStrategy=sRGB");
command.add("-sDEVICE=pdfwrite");
command.add("-dPDFACompatibilityPolicy=2");
command.add("-o");
command.add(tempOutputFile.toString());
command.add(tempInputFile.toString());
ProcessExecutorResult returnCode =
ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT)
.runCommandWithOutputHandling(command);
if (returnCode.getRc() != 0) {
logger.info(
outputFormat + " conversion failed with return code: " + returnCode.getRc());
// Get the original filename without extension
String originalFileName = Filenames.toSimpleFileName(inputFile.getOriginalFilename());
if (originalFileName == null || originalFileName.trim().isEmpty()) {
originalFileName = "output.pdf";
}
String baseFileName =
originalFileName.contains(".")
? originalFileName.substring(0, originalFileName.lastIndexOf('.'))
: originalFileName;
Path tempInputFile = null;
Path tempOutputDir = null;
byte[] fileBytes;
try {
byte[] pdfBytesOutput = Files.readAllBytes(tempOutputFile);
// Return the optimized PDF as a response
String outputFilename =
Filenames.toSimpleFileName(inputFile.getOriginalFilename())
.replaceFirst("[.][^.]+$", "")
+ "_PDFA.pdf";
// Save uploaded file to temp location
tempInputFile = Files.createTempFile("input_", ".pdf");
inputFile.transferTo(tempInputFile);
// Create temp output directory
tempOutputDir = Files.createTempDirectory("output_");
// Determine PDF/A filter based on requested format
String pdfFilter =
"pdfa".equals(outputFormat)
? "writer_pdf_Export:{'SelectPdfVersion':{'Value':'2'}}:writer_pdf_Export"
: "writer_pdf_Export:{'SelectPdfVersion':{'Value':'1'}}:writer_pdf_Export";
// Prepare LibreOffice command
List<String> command =
new ArrayList<>(
Arrays.asList(
"soffice",
"--headless",
"--nologo",
"--convert-to",
"pdf:" + pdfFilter,
"--outdir",
tempOutputDir.toString(),
tempInputFile.toString()));
ProcessExecutorResult returnCode =
ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE)
.runCommandWithOutputHandling(command);
if (returnCode.getRc() != 0) {
logger.error("PDF/A conversion failed with return code: {}", returnCode.getRc());
throw new RuntimeException("PDF/A conversion failed");
}
// Get the output file
File[] outputFiles = tempOutputDir.toFile().listFiles();
if (outputFiles == null || outputFiles.length != 1) {
throw new RuntimeException(
"Expected exactly one output file but found "
+ (outputFiles == null ? "none" : outputFiles.length));
}
fileBytes = FileUtils.readFileToByteArray(outputFiles[0]);
String outputFilename = baseFileName + "_PDFA.pdf";
return WebResponseUtils.bytesToWebResponse(
pdfBytesOutput, outputFilename, MediaType.APPLICATION_PDF);
fileBytes, outputFilename, MediaType.APPLICATION_PDF);
} finally {
// Clean up the temporary files
Files.deleteIfExists(tempInputFile);
Files.deleteIfExists(tempOutputFile);
// Clean up temporary files
if (tempInputFile != null) {
Files.deleteIfExists(tempInputFile);
}
if (tempOutputDir != null) {
FileUtils.deleteDirectory(tempOutputDir.toFile());
}
}
}
}

View File

@@ -1,12 +1,12 @@
package stirling.software.SPDF.controller.api.converters;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.QuoteMode;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ContentDisposition;
@@ -18,21 +18,23 @@ import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.opencsv.CSVWriter;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.controller.api.CropController;
import stirling.software.SPDF.controller.api.strippers.PDFTableStripper;
import stirling.software.SPDF.model.api.extract.PDFFilePage;
import stirling.software.SPDF.pdf.FlexibleCSVWriter;
import technology.tabula.ObjectExtractor;
import technology.tabula.Page;
import technology.tabula.Table;
import technology.tabula.extractors.SpreadsheetExtractionAlgorithm;
import technology.tabula.writers.Writer;
@RestController
@RequestMapping("/api/v1/convert")
@Tag(name = "Convert", description = "Convert APIs")
public class ExtractCSVController {
private static final Logger logger = LoggerFactory.getLogger(CropController.class);
private static final Logger logger = LoggerFactory.getLogger(ExtractCSVController.class);
@PostMapping(value = "/pdf/csv", consumes = "multipart/form-data")
@Operation(
@@ -40,57 +42,16 @@ public class ExtractCSVController {
description =
"This operation takes an input PDF file and returns CSV file of whole page. Input:PDF Output:CSV Type:SISO")
public ResponseEntity<String> PdfToCsv(@ModelAttribute PDFFilePage form) throws Exception {
ArrayList<String> tableData = new ArrayList<>();
int columnsCount = 0;
try (PDDocument document = Loader.loadPDF(form.getFileInput().getBytes())) {
final double res = 72; // PDF units are at 72 DPI
PDFTableStripper stripper = new PDFTableStripper();
PDPage pdPage = document.getPage(form.getPageId() - 1);
stripper.extractTable(pdPage);
columnsCount = stripper.getColumns();
for (int c = 0; c < columnsCount; ++c) {
for (int r = 0; r < stripper.getRows(); ++r) {
tableData.add(stripper.getText(r, c));
}
}
}
ArrayList<String> notEmptyColumns = new ArrayList<>();
for (String item : tableData) {
if (!item.trim().isEmpty()) {
notEmptyColumns.add(item);
} else {
columnsCount--;
}
}
List<String> fullTable =
notEmptyColumns.stream()
.map(
(entity) ->
entity.replace('\n', ' ')
.replace('\r', ' ')
.trim()
.replaceAll("\\s{2,}", "|"))
.toList();
int rowsCount = fullTable.get(0).split("\\|").length;
ArrayList<String> headersList = getTableHeaders(columnsCount, fullTable);
ArrayList<String> recordList = getRecordsList(rowsCount, fullTable);
if (headersList.size() == 0 && recordList.size() == 0) {
throw new Exception("No table detected, no headers or records found");
}
StringWriter writer = new StringWriter();
try (CSVWriter csvWriter = new CSVWriter(writer)) {
csvWriter.writeNext(headersList.toArray(new String[0]));
for (String record : recordList) {
csvWriter.writeNext(record.split("\\|"));
try (PDDocument document = Loader.loadPDF(form.getFileInput().getBytes())) {
CSVFormat format =
CSVFormat.EXCEL.builder().setEscape('"').setQuoteMode(QuoteMode.ALL).build();
Writer csvWriter = new FlexibleCSVWriter(format);
SpreadsheetExtractionAlgorithm sea = new SpreadsheetExtractionAlgorithm();
try (ObjectExtractor extractor = new ObjectExtractor(document)) {
Page page = extractor.extract(form.getPageId());
List<Table> tables = sea.extract(page);
csvWriter.write(writer, tables);
}
}
@@ -107,33 +68,4 @@ public class ExtractCSVController {
return ResponseEntity.ok().headers(headers).body(writer.toString());
}
private ArrayList<String> getRecordsList(int rowsCounts, List<String> items) {
ArrayList<String> recordsList = new ArrayList<>();
for (int b = 1; b < rowsCounts; b++) {
StringBuilder strbldr = new StringBuilder();
for (int i = 0; i < items.size(); i++) {
String[] parts = items.get(i).split("\\|");
strbldr.append(parts[b]);
if (i != items.size() - 1) {
strbldr.append("|");
}
}
recordsList.add(strbldr.toString());
}
return recordsList;
}
private ArrayList<String> getTableHeaders(int columnsCount, List<String> items) {
ArrayList<String> resultList = new ArrayList<>();
for (int i = 0; i < columnsCount; i++) {
String[] parts = items.get(i).split("\\|");
resultList.add(parts[0]);
}
return resultList;
}
}

View File

@@ -10,7 +10,6 @@ import java.util.List;
import javax.imageio.ImageIO;
import org.apache.commons.io.FileUtils;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.pdmodel.PDDocument;
@@ -53,6 +52,54 @@ public class CompressController {
this.pdfDocumentFactory = pdfDocumentFactory;
}
private void compressImagesInPDF(Path pdfFile, double initialScaleFactor) throws Exception {
byte[] fileBytes = Files.readAllBytes(pdfFile);
try (PDDocument doc = Loader.loadPDF(fileBytes)) {
double scaleFactor = initialScaleFactor;
for (PDPage page : doc.getPages()) {
PDResources res = page.getResources();
if (res != null && res.getXObjectNames() != null) {
for (COSName name : res.getXObjectNames()) {
PDXObject xobj = res.getXObject(name);
if (xobj instanceof PDImageXObject) {
PDImageXObject image = (PDImageXObject) xobj;
BufferedImage bufferedImage = image.getImage();
int newWidth = (int) (bufferedImage.getWidth() * scaleFactor);
int newHeight = (int) (bufferedImage.getHeight() * scaleFactor);
if (newWidth == 0 || newHeight == 0) {
continue;
}
Image scaledImage =
bufferedImage.getScaledInstance(
newWidth, newHeight, Image.SCALE_SMOOTH);
BufferedImage scaledBufferedImage =
new BufferedImage(
newWidth, newHeight, BufferedImage.TYPE_INT_RGB);
scaledBufferedImage.getGraphics().drawImage(scaledImage, 0, 0, null);
ByteArrayOutputStream compressedImageStream =
new ByteArrayOutputStream();
ImageIO.write(scaledBufferedImage, "jpeg", compressedImageStream);
byte[] imageBytes = compressedImageStream.toByteArray();
compressedImageStream.close();
PDImageXObject compressedImage =
PDImageXObject.createFromByteArray(
doc, imageBytes, image.getCOSObject().toString());
res.put(name, compressedImage);
}
}
}
}
doc.save(pdfFile.toString());
}
}
@PostMapping(consumes = "multipart/form-data", value = "/compress-pdf")
@Operation(
summary = "Optimize PDF file",
@@ -75,209 +122,92 @@ public class CompressController {
autoMode = true;
}
// Save the uploaded file to a temporary location
Path tempInputFile = Files.createTempFile("input_", ".pdf");
inputFile.transferTo(tempInputFile.toFile());
long inputFileSize = Files.size(tempInputFile);
// Prepare the output file path
Path tempOutputFile = null;
byte[] pdfBytes;
try {
tempOutputFile = Files.createTempFile("output_", ".pdf");
// Determine initial optimization level based on expected size reduction, only if in
// autoMode
if (autoMode) {
double sizeReductionRatio = expectedOutputSize / (double) inputFileSize;
if (sizeReductionRatio > 0.7) {
optimizeLevel = 1;
} else if (sizeReductionRatio > 0.5) {
optimizeLevel = 2;
} else if (sizeReductionRatio > 0.35) {
optimizeLevel = 3;
} else {
optimizeLevel = 3;
}
optimizeLevel = determineOptimizeLevel(sizeReductionRatio);
}
boolean sizeMet = false;
while (!sizeMet && optimizeLevel <= 4) {
// Prepare the Ghostscript command
List<String> command = new ArrayList<>();
command.add("gs");
command.add("-sDEVICE=pdfwrite");
command.add("-dCompatibilityLevel=1.5");
while (!sizeMet && optimizeLevel <= 9) {
switch (optimizeLevel) {
case 1:
command.add("-dPDFSETTINGS=/prepress");
break;
case 2:
command.add("-dPDFSETTINGS=/printer");
break;
case 3:
command.add("-dPDFSETTINGS=/ebook");
break;
case 4:
command.add("-dPDFSETTINGS=/screen");
break;
default:
command.add("-dPDFSETTINGS=/default");
// Apply additional image compression for levels 6-9
if (optimizeLevel >= 6) {
// Calculate scale factor based on optimization level
double scaleFactor =
switch (optimizeLevel) {
case 6 -> 0.9; // 90% of original size
case 7 -> 0.8; // 80% of original size
case 8 -> 0.65; // 70% of original size
case 9 -> 0.5; // 60% of original size
default -> 1.0;
};
compressImagesInPDF(tempInputFile, scaleFactor);
}
command.add("-dNOPAUSE");
command.add("-dQUIET");
command.add("-dBATCH");
command.add("-sOutputFile=" + tempOutputFile.toString());
// Run QPDF optimization
List<String> command = new ArrayList<>();
command.add("qpdf");
if (request.getNormalize()) {
command.add("--normalize-content=y");
}
if (request.getLinearize()) {
command.add("--linearize");
}
command.add("--optimize-images");
command.add("--recompress-flate");
command.add("--compression-level=" + optimizeLevel);
command.add("--compress-streams=y");
command.add("--object-streams=generate");
command.add(tempInputFile.toString());
command.add(tempOutputFile.toString());
ProcessExecutorResult returnCode =
ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT)
.runCommandWithOutputHandling(command);
ProcessExecutorResult returnCode = null;
try {
returnCode =
ProcessExecutor.getInstance(ProcessExecutor.Processes.QPDF)
.runCommandWithOutputHandling(command);
} catch (Exception e) {
if (returnCode != null && returnCode.getRc() != 3) {
throw e;
}
}
// Check if file size is within expected size or not auto mode so instantly finish
// Check if file size is within expected size or not auto mode
long outputFileSize = Files.size(tempOutputFile);
if (outputFileSize <= expectedOutputSize || !autoMode) {
sizeMet = true;
} else {
// Increase optimization level for next iteration
optimizeLevel++;
if (autoMode && optimizeLevel > 4) {
logger.info("Skipping level 5 due to bad results in auto mode");
optimizeLevel =
incrementOptimizeLevel(
optimizeLevel, outputFileSize, expectedOutputSize);
if (autoMode && optimizeLevel > 9) {
logger.info("Maximum compression level reached in auto mode");
sizeMet = true;
} else {
logger.info(
"Increasing ghostscript optimisation level to " + optimizeLevel);
}
}
}
if (expectedOutputSize != null && autoMode) {
long outputFileSize = Files.size(tempOutputFile);
byte[] fileBytes = Files.readAllBytes(tempOutputFile);
if (outputFileSize > expectedOutputSize) {
try (PDDocument doc = Loader.loadPDF(fileBytes)) {
long previousFileSize = 0;
double scaleFactorConst = 0.9f;
double scaleFactor = 0.9f;
while (true) {
for (PDPage page : doc.getPages()) {
PDResources res = page.getResources();
if (res != null && res.getXObjectNames() != null) {
for (COSName name : res.getXObjectNames()) {
PDXObject xobj = res.getXObject(name);
if (xobj != null && xobj instanceof PDImageXObject) {
PDImageXObject image = (PDImageXObject) xobj;
// Get the image in BufferedImage format
BufferedImage bufferedImage = image.getImage();
// Calculate the new dimensions
int newWidth =
(int)
(bufferedImage.getWidth()
* scaleFactorConst);
int newHeight =
(int)
(bufferedImage.getHeight()
* scaleFactorConst);
// If the new dimensions are zero, skip this iteration
if (newWidth == 0 || newHeight == 0) {
continue;
}
// Otherwise, proceed with the scaling
Image scaledImage =
bufferedImage.getScaledInstance(
newWidth,
newHeight,
Image.SCALE_SMOOTH);
// Convert the scaled image back to a BufferedImage
BufferedImage scaledBufferedImage =
new BufferedImage(
newWidth,
newHeight,
BufferedImage.TYPE_INT_RGB);
scaledBufferedImage
.getGraphics()
.drawImage(scaledImage, 0, 0, null);
// Compress the scaled image
ByteArrayOutputStream compressedImageStream =
new ByteArrayOutputStream();
ImageIO.write(
scaledBufferedImage,
"jpeg",
compressedImageStream);
byte[] imageBytes = compressedImageStream.toByteArray();
compressedImageStream.close();
PDImageXObject compressedImage =
PDImageXObject.createFromByteArray(
doc,
imageBytes,
image.getCOSObject().toString());
// Replace the image in the resources with the
// compressed
// version
res.put(name, compressedImage);
}
}
}
}
// save the document to tempOutputFile again
doc.save(tempOutputFile.toString());
long currentSize = Files.size(tempOutputFile);
// Check if the overall PDF size is still larger than expectedOutputSize
if (currentSize > expectedOutputSize) {
// Log the current file size and scaleFactor
logger.info(
"Current file size: "
+ FileUtils.byteCountToDisplaySize(currentSize));
logger.info("Current scale factor: " + scaleFactor);
// The file is still too large, reduce scaleFactor and try again
scaleFactor *= 0.9f; // reduce scaleFactor by 10%
// Avoid scaleFactor being too small, causing the image to shrink to
// 0
if (scaleFactor < 0.2f || previousFileSize == currentSize) {
throw new RuntimeException(
"Could not reach the desired size without excessively degrading image quality, lowest size recommended is "
+ FileUtils.byteCountToDisplaySize(currentSize)
+ ", "
+ currentSize
+ " bytes");
}
previousFileSize = currentSize;
} else {
// The file is small enough, break the loop
break;
}
}
}
}
}
// Read the optimized PDF file
pdfBytes = Files.readAllBytes(tempOutputFile);
Path finalFile = tempOutputFile;
// Check if optimized file is larger than the original
if (pdfBytes.length > inputFileSize) {
// Log the occurrence
logger.warn(
"Optimized file is larger than the original. Returning the original file instead.");
// Read the original file again
finalFile = tempInputFile;
}
// Return the optimized PDF as a response
String outputFilename =
Filenames.toSimpleFileName(inputFile.getOriginalFilename())
.replaceFirst("[.][^.]+$", "")
@@ -286,10 +216,31 @@ public class CompressController {
pdfDocumentFactory.load(finalFile.toFile()), outputFilename);
} finally {
// Clean up the temporary files
// deleted by multipart file handler deu to transferTo?
// Files.deleteIfExists(tempInputFile);
Files.deleteIfExists(tempOutputFile);
}
}
private int determineOptimizeLevel(double sizeReductionRatio) {
if (sizeReductionRatio > 0.9) return 1;
if (sizeReductionRatio > 0.8) return 2;
if (sizeReductionRatio > 0.7) return 3;
if (sizeReductionRatio > 0.6) return 4;
if (sizeReductionRatio > 0.5) return 5;
if (sizeReductionRatio > 0.4) return 6;
if (sizeReductionRatio > 0.3) return 7;
if (sizeReductionRatio > 0.2) return 8;
return 9;
}
private int incrementOptimizeLevel(int currentLevel, long currentSize, long targetSize) {
double currentRatio = currentSize / (double) targetSize;
logger.info("Current compression ratio: {}", String.format("%.2f", currentRatio));
if (currentRatio > 2.0) {
return Math.min(9, currentLevel + 3);
} else if (currentRatio > 1.5) {
return Math.min(9, currentLevel + 2);
}
return Math.min(9, currentLevel + 1);
}
}

View File

@@ -58,7 +58,7 @@ public class FakeScanControllerWIP {
@Operation(
summary = "Repair a PDF file",
description =
"This endpoint repairs a given PDF file by running Ghostscript command. The PDF is first saved to a temporary location, repaired, read back, and then returned as a response.")
"This endpoint repairs a given PDF file by running qpdf command. The PDF is first saved to a temporary location, repaired, read back, and then returned as a response.")
public ResponseEntity<byte[]> fakeScan(@ModelAttribute PDFFile request) throws IOException {
MultipartFile inputFile = request.getFileInput();

View File

@@ -14,6 +14,8 @@ import org.apache.pdfbox.pdmodel.PDDocumentInformation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@@ -26,6 +28,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.misc.MetadataRequest;
import stirling.software.SPDF.utils.WebResponseUtils;
import stirling.software.SPDF.utils.propertyeditor.StringToMapPropertyEditor;
@RestController
@RequestMapping("/api/v1/misc")
@@ -44,6 +47,11 @@ public class MetadataController {
return entry;
}
@InitBinder
public void initBinder(WebDataBinder binder) {
binder.registerCustomEditor(Map.class, "allRequestParams", new StringToMapPropertyEditor());
}
@PostMapping(consumes = "multipart/form-data", value = "/update-metadata")
@Operation(
summary = "Update metadata of a PDF file",

View File

@@ -1,19 +1,29 @@
package stirling.software.SPDF.controller.api.misc;
import java.io.ByteArrayInputStream;
import java.awt.image.BufferedImage;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import javax.imageio.ImageIO;
import org.apache.pdfbox.multipdf.PDFMergerUtility;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.rendering.PDFRenderer;
import org.apache.pdfbox.text.PDFTextStripper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
@@ -23,24 +33,31 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import io.github.pixee.security.BoundedLineReader;
import io.github.pixee.security.Filenames;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.api.misc.ProcessPdfWithOcrRequest;
import stirling.software.SPDF.service.CustomPDDocumentFactory;
import stirling.software.SPDF.utils.ProcessExecutor;
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@RequestMapping("/api/v1/misc")
@Tag(name = "Misc", description = "Miscellaneous APIs")
@Slf4j
public class OCRController {
@Autowired ApplicationProperties applicationProperties;
@Autowired private ApplicationProperties applicationProperties;
private final CustomPDDocumentFactory pdfDocumentFactory;
@Autowired
public OCRController(CustomPDDocumentFactory pdfDocumentFactory) {
this.pdfDocumentFactory = pdfDocumentFactory;
}
/** Gets the list of available Tesseract languages from the tessdata directory */
public List<String> getAvailableTesseractLanguages() {
String tessdataDir = applicationProperties.getSystem().getTessdataDir();
File[] files = new File(tessdataDir).listFiles();
@@ -54,196 +71,163 @@ public class OCRController {
.collect(Collectors.toList());
}
private final CustomPDDocumentFactory pdfDocumentFactory;
@Autowired
public OCRController(CustomPDDocumentFactory pdfDocumentFactory) {
this.pdfDocumentFactory = pdfDocumentFactory;
}
@PostMapping(consumes = "multipart/form-data", value = "/ocr-pdf")
@Operation(
summary = "Process a PDF file with OCR",
description =
"This endpoint processes a PDF file using OCR (Optical Character Recognition). Users can specify languages, sidecar, deskew, clean, cleanFinal, ocrType, ocrRenderType, and removeImagesAfter options. Input:PDF Output:PDF Type:SI-Conditional")
public ResponseEntity<byte[]> processPdfWithOCR(
@ModelAttribute ProcessPdfWithOcrRequest request)
throws IOException, InterruptedException {
MultipartFile inputFile = request.getFileInput();
List<String> selectedLanguages = request.getLanguages();
Boolean sidecar = request.isSidecar();
Boolean deskew = request.isDeskew();
Boolean clean = request.isClean();
Boolean cleanFinal = request.isCleanFinal();
List<String> languages = request.getLanguages();
String ocrType = request.getOcrType();
String ocrRenderType = request.getOcrRenderType();
Boolean removeImagesAfter = request.isRemoveImagesAfter();
// --output-type pdfa
if (selectedLanguages == null || selectedLanguages.isEmpty()) {
throw new IOException("Please select at least one language.");
}
if (!"hocr".equals(ocrRenderType) && !"sandwich".equals(ocrRenderType)) {
throw new IOException("ocrRenderType wrong");
}
Path tempDir = Files.createTempDirectory("ocr_process");
Path tempInputFile = tempDir.resolve("input.pdf");
Path tempOutputDir = tempDir.resolve("output");
Path tempImagesDir = tempDir.resolve("images");
Path finalOutputFile = tempDir.resolve("final_output.pdf");
// Get available Tesseract languages
List<String> availableLanguages = getAvailableTesseractLanguages();
// Validate selected languages
selectedLanguages =
selectedLanguages.stream().filter(availableLanguages::contains).toList();
if (selectedLanguages.isEmpty()) {
throw new IOException("None of the selected languages are valid.");
}
// Save the uploaded file to a temporary location
Path tempInputFile = Files.createTempFile("input_", ".pdf");
Path tempOutputFile = Files.createTempFile("output_", ".pdf");
Path sidecarTextPath = null;
Files.createDirectories(tempOutputDir);
Files.createDirectories(tempImagesDir);
try {
// Save input file
inputFile.transferTo(tempInputFile.toFile());
PDFMergerUtility merger = new PDFMergerUtility();
merger.setDestinationFileName(finalOutputFile.toString());
// Run OCR Command
String languageOption = String.join("+", selectedLanguages);
try (PDDocument document = pdfDocumentFactory.load(tempInputFile.toFile())) {
PDFRenderer pdfRenderer = new PDFRenderer(document);
int pageCount = document.getNumberOfPages();
List<String> command =
new ArrayList<>(
Arrays.asList(
"ocrmypdf",
"--verbose",
"2",
"--output-type",
"pdf",
"--pdf-renderer",
ocrRenderType));
for (int pageNum = 0; pageNum < pageCount; pageNum++) {
PDPage page = document.getPage(pageNum);
boolean hasText = false;
if (sidecar != null && sidecar) {
sidecarTextPath = Files.createTempFile("sidecar", ".txt");
command.add("--sidecar");
command.add(sidecarTextPath.toString());
}
// Check for existing text
try (PDDocument tempDoc = new PDDocument()) {
tempDoc.addPage(page);
PDFTextStripper stripper = new PDFTextStripper();
hasText = !stripper.getText(tempDoc).trim().isEmpty();
}
if (deskew != null && deskew) {
command.add("--deskew");
}
if (clean != null && clean) {
command.add("--clean");
}
if (cleanFinal != null && cleanFinal) {
command.add("--clean-final");
}
if (ocrType != null && !"".equals(ocrType)) {
if ("skip-text".equals(ocrType)) {
command.add("--skip-text");
} else if ("force-ocr".equals(ocrType)) {
command.add("--force-ocr");
} else if ("Normal".equals(ocrType)) {
boolean shouldOcr =
switch (ocrType) {
case "skip-text" -> !hasText;
case "force-ocr" -> true;
default -> true;
};
Path pageOutputPath =
tempOutputDir.resolve(String.format("page_%d.pdf", pageNum));
if (shouldOcr) {
// Convert page to image
BufferedImage image = pdfRenderer.renderImageWithDPI(pageNum, 300);
Path imagePath =
tempImagesDir.resolve(String.format("page_%d.png", pageNum));
ImageIO.write(image, "png", imagePath.toFile());
// Build OCR command
List<String> command = new ArrayList<>();
command.add("tesseract");
command.add(imagePath.toString());
command.add(
tempOutputDir
.resolve(String.format("page_%d", pageNum))
.toString());
command.add("-l");
command.add(String.join("+", languages));
command.add("pdf"); // Always output PDF
ProcessBuilder pb = new ProcessBuilder(command);
Process process = pb.start();
// Capture any error output
try (BufferedReader reader =
new BufferedReader(
new InputStreamReader(process.getErrorStream()))) {
String line;
while ((line = BoundedLineReader.readLine(reader, 5_000_000)) != null) {
log.debug("Tesseract: {}", line);
}
}
int exitCode = process.waitFor();
if (exitCode != 0) {
throw new RuntimeException(
"Tesseract failed with exit code: " + exitCode);
}
// Add OCR'd PDF to merger
merger.addSource(pageOutputPath.toFile());
} else {
// Save original page without OCR
try (PDDocument pageDoc = new PDDocument()) {
pageDoc.addPage(page);
pageDoc.save(pageOutputPath.toFile());
merger.addSource(pageOutputPath.toFile());
}
}
}
}
command.addAll(
Arrays.asList(
"--language",
languageOption,
tempInputFile.toString(),
tempOutputFile.toString()));
// Merge all pages into final PDF
merger.mergeDocuments(null);
// Run CLI command
ProcessExecutorResult result =
ProcessExecutor.getInstance(ProcessExecutor.Processes.OCR_MY_PDF)
.runCommandWithOutputHandling(command);
if (result.getRc() != 0
&& result.getMessages().contains("multiprocessing/synchronize.py")
&& result.getMessages()
.contains("OSError: [Errno 38] Function not implemented")) {
command.add("--jobs");
command.add("1");
result =
ProcessExecutor.getInstance(ProcessExecutor.Processes.OCR_MY_PDF)
.runCommandWithOutputHandling(command);
}
// Remove images from the OCR processed PDF if the flag is set to true
if (removeImagesAfter != null && removeImagesAfter) {
Path tempPdfWithoutImages = Files.createTempFile("output_", "_no_images.pdf");
List<String> gsCommand =
Arrays.asList(
"gs",
"-sDEVICE=pdfwrite",
"-dFILTERIMAGE",
"-o",
tempPdfWithoutImages.toString(),
tempOutputFile.toString());
ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT)
.runCommandWithOutputHandling(gsCommand);
tempOutputFile = tempPdfWithoutImages;
}
// Read the OCR processed PDF file
byte[] pdfBytes = pdfDocumentFactory.loadToBytes(tempOutputFile.toFile());
// Return the OCR processed PDF as a response
// Read the final PDF file
byte[] pdfContent = Files.readAllBytes(finalOutputFile);
String outputFilename =
Filenames.toSimpleFileName(inputFile.getOriginalFilename())
.replaceFirst("[.][^.]+$", "")
+ "_OCR.pdf";
if (sidecar != null && sidecar) {
// Create a zip file containing both the PDF and the text file
String outputZipFilename =
Filenames.toSimpleFileName(inputFile.getOriginalFilename())
.replaceFirst("[.][^.]+$", "")
+ "_OCR.zip";
Path tempZipFile = Files.createTempFile("output_", ".zip");
return ResponseEntity.ok()
.header(
"Content-Disposition",
"attachment; filename=\"" + outputFilename + "\"")
.contentType(MediaType.APPLICATION_PDF)
.body(pdfContent);
try (ZipOutputStream zipOut =
new ZipOutputStream(new FileOutputStream(tempZipFile.toFile()))) {
// Add PDF file to the zip
ZipEntry pdfEntry = new ZipEntry(outputFilename);
zipOut.putNextEntry(pdfEntry);
try (ByteArrayInputStream pdfInputStream = new ByteArrayInputStream(pdfBytes)) {
byte[] buffer = new byte[1024];
int length;
while ((length = pdfInputStream.read(buffer)) != -1) {
zipOut.write(buffer, 0, length);
}
}
zipOut.closeEntry();
// Add text file to the zip
ZipEntry txtEntry = new ZipEntry(outputFilename.replace(".pdf", ".txt"));
zipOut.putNextEntry(txtEntry);
Files.copy(sidecarTextPath, zipOut);
zipOut.closeEntry();
}
byte[] zipBytes = Files.readAllBytes(tempZipFile);
// Clean up the temporary zip file
Files.deleteIfExists(tempZipFile);
Files.deleteIfExists(tempOutputFile);
Files.deleteIfExists(sidecarTextPath);
// Return the zip file containing both the PDF and the text file
return WebResponseUtils.bytesToWebResponse(
zipBytes, outputZipFilename, MediaType.APPLICATION_OCTET_STREAM);
} else {
// Return the OCR processed PDF as a response
Files.deleteIfExists(tempOutputFile);
return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename);
}
} finally {
// Clean up the temporary files
Files.deleteIfExists(tempOutputFile);
// Comment out as transferTo makes multipart handle cleanup
// Files.deleteIfExists(tempInputFile);
if (sidecarTextPath != null) {
Files.deleteIfExists(sidecarTextPath);
// Clean up temporary files
deleteDirectory(tempDir);
}
}
private void addFileToZip(File file, String filename, ZipOutputStream zipOut)
throws IOException {
if (!file.exists()) {
log.warn("File {} does not exist, skipping", file);
return;
}
try (FileInputStream fis = new FileInputStream(file)) {
ZipEntry zipEntry = new ZipEntry(filename);
zipOut.putNextEntry(zipEntry);
byte[] buffer = new byte[1024];
int length;
while ((length = fis.read(buffer)) >= 0) {
zipOut.write(buffer, 0, length);
}
zipOut.closeEntry();
}
}
private void deleteDirectory(Path directory) {
try {
Files.walk(directory)
.sorted(Comparator.reverseOrder())
.forEach(
path -> {
try {
Files.delete(path);
} catch (IOException e) {
log.error("Error deleting {}: {}", path, e.getMessage());
}
});
} catch (IOException e) {
log.error("Error walking directory {}: {}", directory, e.getMessage());
}
}
}

View File

@@ -44,30 +44,29 @@ public class RepairController {
@Operation(
summary = "Repair a PDF file",
description =
"This endpoint repairs a given PDF file by running Ghostscript command. The PDF is first saved to a temporary location, repaired, read back, and then returned as a response. Input:PDF Output:PDF Type:SISO")
"This endpoint repairs a given PDF file by running qpdf command. The PDF is first saved to a temporary location, repaired, read back, and then returned as a response. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> repairPdf(@ModelAttribute PDFFile request)
throws IOException, InterruptedException {
MultipartFile inputFile = request.getFileInput();
// Save the uploaded file to a temporary location
Path tempInputFile = Files.createTempFile("input_", ".pdf");
Path tempOutputFile = Files.createTempFile("output_", ".pdf");
byte[] pdfBytes = null;
inputFile.transferTo(tempInputFile.toFile());
try {
List<String> command = new ArrayList<>();
command.add("gs");
command.add("-o");
command.add(tempOutputFile.toString());
command.add("-sDEVICE=pdfwrite");
command.add("qpdf");
command.add("--replace-input"); // Automatically fixes problems it can
command.add("--qdf"); // Linearizes and normalizes PDF structure
command.add("--object-streams=disable"); // Can help with some corruptions
command.add(tempInputFile.toString());
ProcessExecutorResult returnCode =
ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT)
ProcessExecutor.getInstance(ProcessExecutor.Processes.QPDF)
.runCommandWithOutputHandling(command);
// Read the optimized PDF file
pdfBytes = pdfDocumentFactory.loadToBytes(tempOutputFile.toFile());
pdfBytes = pdfDocumentFactory.loadToBytes(tempInputFile.toFile());
// Return the optimized PDF as a response
String outputFilename =
@@ -78,7 +77,6 @@ public class RepairController {
} finally {
// Clean up the temporary files
Files.deleteIfExists(tempInputFile);
Files.deleteIfExists(tempOutputFile);
}
}
}

View File

@@ -13,12 +13,14 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.misc.ReplaceAndInvertColorRequest;
import stirling.software.SPDF.service.misc.ReplaceAndInvertColorService;
@RestController
@RequestMapping("/api/v1/misc")
@Tag(name = "Misc", description = "Miscellaneous APIs")
public class ReplaceAndInvertColorController {
private ReplaceAndInvertColorService replaceAndInvertColorService;

View File

@@ -229,10 +229,22 @@ public class StampController {
calculatePositionY(
pageSize, position, calculateTextCapHeight(font, fontSize), margin);
}
// Split the stampText into multiple lines
String[] lines = stampText.split("\\\\n");
// Calculate dynamic line height based on font ascent and descent
float ascent = font.getFontDescriptor().getAscent();
float descent = font.getFontDescriptor().getDescent();
float lineHeight = ((ascent - descent) / 1000) * fontSize;
contentStream.beginText();
contentStream.setTextMatrix(Matrix.getRotateInstance(Math.toRadians(rotation), x, y));
contentStream.showText(stampText);
for (int i = 0; i < lines.length; i++) {
String line = lines[i];
// Set the text matrix for each line with rotation
contentStream.setTextMatrix(
Matrix.getRotateInstance(Math.toRadians(rotation), x, y - (i * lineHeight)));
contentStream.showText(line);
}
contentStream.endText();
}

View File

@@ -8,6 +8,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.nio.file.Files;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
@@ -21,6 +22,7 @@ import java.security.cert.X509Certificate;
import java.util.Calendar;
import java.util.List;
import org.apache.commons.io.FileUtils;
import org.apache.pdfbox.examples.signature.CreateSignatureBase;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
@@ -92,7 +94,7 @@ public class CertSignController {
}
class CreateSignature extends CreateSignatureBase {
File imageFile;
File logoFile;
public CreateSignature(KeyStore keystore, char[] pin)
throws KeyStoreException,
@@ -102,11 +104,17 @@ public class CertSignController {
CertificateException {
super(keystore, pin);
ClassPathResource resource = new ClassPathResource("static/images/signature.png");
imageFile = resource.getFile();
try (InputStream is = resource.getInputStream()) {
logoFile = Files.createTempFile("signature", ".png").toFile();
FileUtils.copyInputStreamToFile(is, logoFile);
} catch (IOException e) {
logger.error("Failed to load image signature file");
throw e;
}
}
public InputStream createVisibleSignature(
PDDocument srcDoc, PDSignature signature, Integer pageNumber, Boolean showImage)
PDDocument srcDoc, PDSignature signature, Integer pageNumber, Boolean showLogo)
throws IOException {
// modified from org.apache.pdfbox.examples.signature.CreateVisibleSignature2
try (PDDocument doc = new PDDocument()) {
@@ -145,7 +153,7 @@ public class CertSignController {
widget.setAppearance(appearance);
try (PDPageContentStream cs = new PDPageContentStream(doc, appearanceStream)) {
if (showImage) {
if (showLogo) {
cs.saveGraphicsState();
PDExtendedGraphicsState extState = new PDExtendedGraphicsState();
extState.setBlendMode(BlendMode.MULTIPLY);
@@ -153,7 +161,7 @@ public class CertSignController {
cs.setGraphicsStateParameters(extState);
cs.transform(Matrix.getScaleInstance(0.08f, 0.08f));
PDImageXObject img =
PDImageXObject.createFromFileByExtension(imageFile, doc);
PDImageXObject.createFromFileByExtension(logoFile, doc);
cs.drawImage(img, 100, 0);
cs.restoreGraphicsState();
}
@@ -219,6 +227,7 @@ public class CertSignController {
String location = request.getLocation();
String name = request.getName();
Integer pageNumber = request.getPageNumber() - 1;
Boolean showLogo = request.isShowLogo();
if (certType == null) {
throw new IllegalArgumentException("Cert type must be provided");
@@ -258,7 +267,8 @@ public class CertSignController {
pageNumber,
name,
location,
reason);
reason,
showLogo);
return WebResponseUtils.boasToWebResponse(
baos,
Filenames.toSimpleFileName(pdf.getOriginalFilename()).replaceFirst("[.][^.]+$", "")
@@ -274,7 +284,8 @@ public class CertSignController {
Integer pageNumber,
String name,
String location,
String reason) {
String reason,
Boolean showLogo) {
try (PDDocument doc = pdfDocumentFactory.load(input)) {
PDSignature signature = new PDSignature();
signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);
@@ -287,7 +298,7 @@ public class CertSignController {
if (showSignature) {
SignatureOptions signatureOptions = new SignatureOptions();
signatureOptions.setVisualSignature(
instance.createVisibleSignature(doc, signature, pageNumber, true));
instance.createVisibleSignature(doc, signature, pageNumber, showLogo));
signatureOptions.setPage(pageNumber);
doc.addSignature(signature, instance, signatureOptions);

View File

@@ -322,27 +322,14 @@ public class GetInfoOnPDF {
PDEncryption pdfEncryption = pdfBoxDoc.getEncryption();
encryption.put("EncryptionAlgorithm", pdfEncryption.getFilter());
encryption.put("KeyLength", pdfEncryption.getLength());
AccessPermission ap = pdfBoxDoc.getCurrentAccessPermission();
if (ap != null) {
ObjectNode permissionsNode = objectMapper.createObjectNode();
permissionsNode.put("CanAssembleDocument", ap.canAssembleDocument());
permissionsNode.put("CanExtractContent", ap.canExtractContent());
permissionsNode.put(
"CanExtractForAccessibility", ap.canExtractForAccessibility());
permissionsNode.put("CanFillInForm", ap.canFillInForm());
permissionsNode.put("CanModify", ap.canModify());
permissionsNode.put("CanModifyAnnotations", ap.canModifyAnnotations());
permissionsNode.put("CanPrint", ap.canPrint());
encryption.set(
"Permissions", permissionsNode); // set the node under "Permissions"
}
// Add other encryption-related properties as needed
} else {
encryption.put("IsEncrypted", false);
}
ObjectNode permissionsNode = objectMapper.createObjectNode();
setNodePermissions(pdfBoxDoc, permissionsNode);
ObjectNode pageInfoParent = objectMapper.createObjectNode();
for (int pageNum = 0; pageNum < pdfBoxDoc.getNumberOfPages(); pageNum++) {
ObjectNode pageInfo = objectMapper.createObjectNode();
@@ -584,6 +571,7 @@ public class GetInfoOnPDF {
jsonOutput.set("DocumentInfo", docInfoNode);
jsonOutput.set("Compliancy", compliancy);
jsonOutput.set("Encryption", encryption);
jsonOutput.set("Permissions", permissionsNode); // set the node under "Permissions"
jsonOutput.set("Other", other);
jsonOutput.set("PerPageInfo", pageInfoParent);
@@ -602,6 +590,24 @@ public class GetInfoOnPDF {
return null;
}
private void setNodePermissions(PDDocument pdfBoxDoc, ObjectNode permissionsNode) {
AccessPermission ap = pdfBoxDoc.getCurrentAccessPermission();
permissionsNode.put("Document Assembly", getPermissionState(ap.canAssembleDocument()));
permissionsNode.put("Extracting Content", getPermissionState(ap.canExtractContent()));
permissionsNode.put(
"Extracting for accessibility",
getPermissionState(ap.canExtractForAccessibility()));
permissionsNode.put("Form Filling", getPermissionState(ap.canFillInForm()));
permissionsNode.put("Modifying", getPermissionState(ap.canModify()));
permissionsNode.put("Modifying annotations", getPermissionState(ap.canModifyAnnotations()));
permissionsNode.put("Printing", getPermissionState(ap.canPrint()));
}
private String getPermissionState(boolean state) {
return state ? "Allowed" : "Not Allowed";
}
private static void addOutlinesToArray(PDOutlineItem outline, ArrayNode arrayNode) {
if (outline == null) return;

View File

@@ -0,0 +1,180 @@
package stirling.software.SPDF.controller.api.security;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateExpiredException;
import java.security.cert.CertificateFactory;
import java.security.cert.CertificateNotYetValidException;
import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPublicKey;
import java.util.ArrayList;
import java.util.List;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cms.CMSProcessable;
import org.bouncycastle.cms.CMSProcessableByteArray;
import org.bouncycastle.cms.CMSSignedData;
import org.bouncycastle.cms.SignerInformation;
import org.bouncycastle.cms.SignerInformationStore;
import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder;
import org.bouncycastle.util.Store;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.security.SignatureValidationRequest;
import stirling.software.SPDF.model.api.security.SignatureValidationResult;
import stirling.software.SPDF.service.CertificateValidationService;
import stirling.software.SPDF.service.CustomPDDocumentFactory;
@RestController
@RequestMapping("/api/v1/security")
@Tag(name = "Security", description = "Security APIs")
public class ValidateSignatureController {
private final CustomPDDocumentFactory pdfDocumentFactory;
private final CertificateValidationService certValidationService;
@Autowired
public ValidateSignatureController(
CustomPDDocumentFactory pdfDocumentFactory,
CertificateValidationService certValidationService) {
this.pdfDocumentFactory = pdfDocumentFactory;
this.certValidationService = certValidationService;
}
@Operation(
summary = "Validate PDF Digital Signature",
description =
"Validates digital signatures in a PDF file against default or custom certificates.")
@PostMapping(value = "/validate-signature")
public ResponseEntity<List<SignatureValidationResult>> validateSignature(
@ModelAttribute SignatureValidationRequest request) throws IOException {
List<SignatureValidationResult> results = new ArrayList<>();
MultipartFile file = request.getFileInput();
// Load custom certificate if provided
X509Certificate customCert = null;
if (request.getCertFile() != null && !request.getCertFile().isEmpty()) {
try (ByteArrayInputStream certStream =
new ByteArrayInputStream(request.getCertFile().getBytes())) {
CertificateFactory cf = CertificateFactory.getInstance("X.509");
customCert = (X509Certificate) cf.generateCertificate(certStream);
} catch (CertificateException e) {
throw new RuntimeException("Invalid certificate file: " + e.getMessage());
}
}
try (PDDocument document = pdfDocumentFactory.load(file.getInputStream())) {
List<PDSignature> signatures = document.getSignatureDictionaries();
for (PDSignature sig : signatures) {
SignatureValidationResult result = new SignatureValidationResult();
try {
byte[] signedContent = sig.getSignedContent(file.getInputStream());
byte[] signatureBytes = sig.getContents(file.getInputStream());
CMSProcessable content = new CMSProcessableByteArray(signedContent);
CMSSignedData signedData = new CMSSignedData(content, signatureBytes);
Store<X509CertificateHolder> certStore = signedData.getCertificates();
SignerInformationStore signerStore = signedData.getSignerInfos();
for (SignerInformation signer : signerStore.getSigners()) {
X509CertificateHolder certHolder =
(X509CertificateHolder)
certStore.getMatches(signer.getSID()).iterator().next();
X509Certificate cert =
new JcaX509CertificateConverter().getCertificate(certHolder);
// Basic signature validation
result.setValid(
signer.verify(
new JcaSimpleSignerInfoVerifierBuilder().build(cert)));
// Perform chain validation
CertificateValidationService.ValidationResult chainResult;
if (customCert != null) {
chainResult =
certValidationService.validateWithCustomCert(cert, customCert);
} else {
chainResult = certValidationService.validateCertificateChain(cert);
}
result.setChainValid(chainResult.isValid());
result.setTrustValid(chainResult.isValid());
result.setNotExpired(!chainResult.isExpired());
// Check if signature was valid at the time of signing
if (sig.getSignDate() != null) {
try {
cert.checkValidity(sig.getSignDate().getTime());
result.setValidAtTimeOfSigning(true);
} catch (CertificateExpiredException
| CertificateNotYetValidException e) {
result.setValidAtTimeOfSigning(false);
}
}
// Set signature info
populateSignatureInfo(result, sig, cert);
}
} catch (Exception e) {
result.setValid(false);
result.setErrorMessage("Signature validation failed: " + e.getMessage());
}
results.add(result);
}
}
return ResponseEntity.ok(results);
}
private void populateSignatureInfo(
SignatureValidationResult result, PDSignature sig, X509Certificate cert) {
result.setSignerName(sig.getName());
result.setSignatureDate(sig.getSignDate().getTime().toString());
result.setReason(sig.getReason());
result.setLocation(sig.getLocation());
result.setIssuerDN(cert.getIssuerX500Principal().getName());
result.setSubjectDN(cert.getSubjectX500Principal().getName());
result.setSerialNumber(cert.getSerialNumber().toString(16));
result.setValidFrom(cert.getNotBefore().toString());
result.setValidUntil(cert.getNotAfter().toString());
result.setSignatureAlgorithm(cert.getSigAlgName());
try {
result.setKeySize(((RSAPublicKey) cert.getPublicKey()).getModulus().bitLength());
} catch (Exception e) {
result.setKeySize(0);
}
result.setVersion(String.valueOf(cert.getVersion()));
List<String> keyUsages = new ArrayList<>();
boolean[] keyUsageFlags = cert.getKeyUsage();
if (keyUsageFlags != null) {
String[] keyUsageLabels = {
"Digital Signature", "Non-Repudiation", "Key Encipherment",
"Data Encipherment", "Key Agreement", "Certificate Signing",
"CRL Signing", "Encipher Only", "Decipher Only"
};
for (int i = 0; i < keyUsageFlags.length; i++) {
if (keyUsageFlags[i]) {
keyUsages.add(keyUsageLabels[i]);
}
}
}
result.setKeyUsages(keyUsages);
result.setSelfSigned(cert.getSubjectX500Principal().equals(cert.getIssuerX500Principal()));
}
}

View File

@@ -187,18 +187,31 @@ public class WatermarkController {
float watermarkHeight = heightSpacer + fontSize * textLines.length;
float pageWidth = page.getMediaBox().getWidth();
float pageHeight = page.getMediaBox().getHeight();
int watermarkRows = (int) (pageHeight / watermarkHeight + 1);
int watermarkCols = (int) (pageWidth / watermarkWidth + 1);
// Calculating the new width and height depending on the angle.
float radians = (float) Math.toRadians(rotation);
float newWatermarkWidth =
(float)
(Math.abs(watermarkWidth * Math.cos(radians))
+ Math.abs(watermarkHeight * Math.sin(radians)));
float newWatermarkHeight =
(float)
(Math.abs(watermarkWidth * Math.sin(radians))
+ Math.abs(watermarkHeight * Math.cos(radians)));
// Calculating the number of rows and columns.
int watermarkRows = (int) (pageHeight / newWatermarkHeight + 1);
int watermarkCols = (int) (pageWidth / newWatermarkWidth + 1);
// Add the text watermark
for (int i = 0; i < watermarkRows; i++) {
for (int j = 0; j < watermarkCols; j++) {
for (int i = 0; i <= watermarkRows; i++) {
for (int j = 0; j <= watermarkCols; j++) {
contentStream.beginText();
contentStream.setTextMatrix(
Matrix.getRotateInstance(
(float) Math.toRadians(rotation),
j * watermarkWidth,
i * watermarkHeight));
j * newWatermarkWidth,
i * newWatermarkHeight));
for (int k = 0; k < textLines.length; ++k) {
contentStream.showText(textLines[k]);

View File

@@ -1,327 +0,0 @@
package stirling.software.SPDF.controller.api.strippers;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import org.apache.fontbox.util.BoundingBox;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.font.PDFont;
import org.apache.pdfbox.pdmodel.font.PDType3Font;
import org.apache.pdfbox.text.PDFTextStripper;
import org.apache.pdfbox.text.PDFTextStripperByArea;
import org.apache.pdfbox.text.TextPosition;
/**
* Class to extract tabular data from a PDF. Works by making a first pass of the page to group all
* nearby text items together, and then inferring a 2D grid from these regions. Each table cell is
* then extracted using a PDFTextStripperByArea object.
*
* <p>Works best when headers are included in the detected region, to ensure representative text in
* every column.
*
* <p>Based upon DrawPrintTextLocations PDFBox example
* (https://svn.apache.org/viewvc/pdfbox/trunk/examples/src/main/java/org/apache/pdfbox/examples/util/DrawPrintTextLocations.java)
*
* @author Beldaz
*/
public class PDFTableStripper extends PDFTextStripper {
/**
* This will print the documents data, for each table cell.
*
* @param args The command line arguments.
* @throws IOException If there is an error parsing the document.
*/
/*
* Used in methods derived from DrawPrintTextLocations
*/
private AffineTransform flipAT;
private AffineTransform rotateAT;
/** Regions updated by calls to writeString */
private Set<Rectangle2D> boxes;
// Border to allow when finding intersections
private double dx = 1.0; // This value works for me, feel free to tweak (or add setter)
private double dy = 0.000; // Rows of text tend to overlap, so need to extend
/** Region in which to find table (otherwise whole page) */
private Rectangle2D regionArea;
/** Number of rows in inferred table */
private int nRows = 0;
/** Number of columns in inferred table */
private int nCols = 0;
/** This is the object that does the text extraction */
private PDFTextStripperByArea regionStripper;
/**
* 1D intervals - used for calculateTableRegions()
*
* @author Beldaz
*/
public static class Interval {
double start;
double end;
public Interval(double start, double end) {
this.start = start;
this.end = end;
}
public void add(Interval col) {
if (col.start < start) start = col.start;
if (col.end > end) end = col.end;
}
public static void addTo(Interval x, LinkedList<Interval> columns) {
int p = 0;
Iterator<Interval> it = columns.iterator();
// Find where x should go
while (it.hasNext()) {
Interval col = it.next();
if (x.end >= col.start) {
if (x.start <= col.end) { // overlaps
x.add(col);
it.remove();
}
break;
}
++p;
}
while (it.hasNext()) {
Interval col = it.next();
if (x.start > col.end) break;
x.add(col);
it.remove();
}
columns.add(p, x);
}
}
/**
* Instantiate a new PDFTableStripper object.
*
* @throws IOException If there is an error loading the properties.
*/
public PDFTableStripper() throws IOException {
super.setShouldSeparateByBeads(false);
regionStripper = new PDFTextStripperByArea();
regionStripper.setSortByPosition(true);
}
/**
* Define the region to group text by.
*
* @param rect The rectangle area to retrieve the text from.
*/
public void setRegion(Rectangle2D rect) {
regionArea = rect;
}
public int getRows() {
return nRows;
}
public int getColumns() {
return nCols;
}
/**
* Get the text for the region, this should be called after extractTable().
*
* @return The text that was identified in that region.
*/
public String getText(int row, int col) {
return regionStripper.getTextForRegion("el" + col + "x" + row);
}
public void extractTable(PDPage pdPage) throws IOException {
setStartPage(getCurrentPageNo());
setEndPage(getCurrentPageNo());
boxes = new HashSet<Rectangle2D>();
// flip y-axis
flipAT = new AffineTransform();
flipAT.translate(0, pdPage.getBBox().getHeight());
flipAT.scale(1, -1);
// page may be rotated
rotateAT = new AffineTransform();
int rotation = pdPage.getRotation();
if (rotation != 0) {
PDRectangle mediaBox = pdPage.getMediaBox();
switch (rotation) {
case 90:
rotateAT.translate(mediaBox.getHeight(), 0);
break;
case 270:
rotateAT.translate(0, mediaBox.getWidth());
break;
case 180:
rotateAT.translate(mediaBox.getWidth(), mediaBox.getHeight());
break;
default:
break;
}
rotateAT.rotate(Math.toRadians(rotation));
}
// Trigger processing of the document so that writeString is called.
try (Writer dummy = new OutputStreamWriter(new ByteArrayOutputStream())) {
super.output = dummy;
super.processPage(pdPage);
}
Rectangle2D[][] regions = calculateTableRegions();
// System.err.println("Drawing " + nCols + "x" + nRows + "="+ nRows*nCols + "
// regions");
for (int i = 0; i < nCols; ++i) {
for (int j = 0; j < nRows; ++j) {
final Rectangle2D region = regions[i][j];
regionStripper.addRegion("el" + i + "x" + j, region);
}
}
regionStripper.extractRegions(pdPage);
}
/**
* Infer a rectangular grid of regions from the boxes field.
*
* @return 2D array of table regions (as Rectangle2D objects). Note that some of these regions
* may have no content.
*/
private Rectangle2D[][] calculateTableRegions() {
// Build up a list of all table regions, based upon the populated
// regions of boxes field. Treats the horizontal and vertical extents
// of each box as distinct
LinkedList<Interval> columns = new LinkedList<Interval>();
LinkedList<Interval> rows = new LinkedList<Interval>();
for (Rectangle2D box : boxes) {
Interval x = new Interval(box.getMinX(), box.getMaxX());
Interval y = new Interval(box.getMinY(), box.getMaxY());
Interval.addTo(x, columns);
Interval.addTo(y, rows);
}
nRows = rows.size();
nCols = columns.size();
Rectangle2D[][] regions = new Rectangle2D[nCols][nRows];
int i = 0;
// Label regions from top left, rather than the transformed orientation
for (Interval column : columns) {
int j = 0;
for (Interval row : rows) {
regions[nCols - i - 1][nRows - j - 1] =
new Rectangle2D.Double(
column.start,
row.start,
column.end - column.start,
row.end - row.start);
++j;
}
++i;
}
return regions;
}
/**
* Register each character's bounding box, updating boxes field to maintain a list of all
* distinct groups of characters.
*
* <p>Overrides the default functionality of PDFTextStripper. Most of this is taken from
* DrawPrintTextLocations.java, with extra steps at end of main loop
*/
@Override
protected void writeString(String string, List<TextPosition> textPositions) throws IOException {
for (TextPosition text : textPositions) {
// glyph space -> user space
// note: text.getTextMatrix() is *not* the Text Matrix, it's the Text Rendering Matrix
AffineTransform at = text.getTextMatrix().createAffineTransform();
PDFont font = text.getFont();
BoundingBox bbox = font.getBoundingBox();
// advance width, bbox height (glyph space)
float xadvance =
font.getWidth(text.getCharacterCodes()[0]); // todo: should iterate all chars
Rectangle2D.Float rect =
new Rectangle2D.Float(0, bbox.getLowerLeftY(), xadvance, bbox.getHeight());
if (font instanceof PDType3Font) {
// bbox and font matrix are unscaled
at.concatenate(font.getFontMatrix().createAffineTransform());
} else {
// bbox and font matrix are already scaled to 1000
at.scale(1 / 1000f, 1 / 1000f);
}
Shape s = at.createTransformedShape(rect);
s = flipAT.createTransformedShape(s);
s = rotateAT.createTransformedShape(s);
//
// Merge character's bounding box with boxes field
//
Rectangle2D bounds = s.getBounds2D();
// Pad sides to detect almost touching boxes
Rectangle2D hitbox = bounds.getBounds2D();
hitbox.add(bounds.getMinX() - dx, bounds.getMinY() - dy);
hitbox.add(bounds.getMaxX() + dx, bounds.getMaxY() + dy);
// Find all overlapping boxes
List<Rectangle2D> intersectList = new ArrayList<Rectangle2D>();
for (Rectangle2D box : boxes) {
if (box.intersects(hitbox)) {
intersectList.add(box);
}
}
// Combine all touching boxes and update
// (NOTE: Potentially this could leave some overlapping boxes un-merged,
// but it's sufficient for now and get's fixed up in calculateTableRegions)
for (Rectangle2D box : intersectList) {
bounds.add(box);
boxes.remove(box);
}
boxes.add(bounds);
}
}
/**
* This method does nothing in this derived class, because beads and regions are incompatible.
* Beads are ignored when stripping by area.
*
* @param aShouldSeparateByBeads The new grouping of beads.
*/
@Override
public final void setShouldSeparateByBeads(boolean aShouldSeparateByBeads) {}
/** Adapted from PDFTextStripperByArea {@inheritDoc} */
@Override
protected void processTextPosition(TextPosition text) {
if (regionArea != null && !regionArea.contains(text.getX(), text.getY())) {
// skip character
} else {
super.processTextPosition(text);
}
}
}

View File

@@ -34,7 +34,9 @@ public class DatabaseWebController {
}
List<FileInfo> backupList = databaseBackupHelper.getBackupList();
model.addAttribute("systemUpdate", backupList);
model.addAttribute("backupFiles", backupList);
model.addAttribute("databaseVersion", databaseBackupHelper.getH2Version());
return "database";
}

View File

@@ -55,6 +55,11 @@ public class HomeWebController {
return "licenses";
}
@GetMapping("/releases")
public String getReleaseNotes(Model model) {
return "releases";
}
@GetMapping("/")
public String home(Model model) {
model.addAttribute("currentPage", "home");

View File

@@ -53,6 +53,13 @@ public class SecurityWebController {
return "security/cert-sign";
}
@GetMapping("/validate-signature")
@Hidden
public String certSignVerifyForm(Model model) {
model.addAttribute("currentPage", "validate-signature");
return "security/validate-signature";
}
@GetMapping("/remove-cert-sign")
@Hidden
public String certUnSignForm(Model model) {

View File

@@ -15,7 +15,7 @@ import stirling.software.SPDF.controller.api.pipeline.UserServiceInterface;
import stirling.software.SPDF.service.SignatureService;
@Controller
@RequestMapping("/api/v1/general/")
@RequestMapping("/api/v1/general")
public class SignatureController {
@Autowired private SignatureService signatureService;

View File

@@ -47,6 +47,7 @@ public class ApplicationProperties {
private AutomaticallyGenerated automaticallyGenerated = new AutomaticallyGenerated();
private EnterpriseEdition enterpriseEdition = new EnterpriseEdition();
private AutoPipeline autoPipeline = new AutoPipeline();
private ProcessExecutor processExecutor = new ProcessExecutor();
@Data
public static class AutoPipeline {
@@ -309,4 +310,98 @@ public class ApplicationProperties {
}
}
}
@Data
public static class ProcessExecutor {
private SessionLimit sessionLimit = new SessionLimit();
private TimeoutMinutes timeoutMinutes = new TimeoutMinutes();
@Data
public static class SessionLimit {
private int libreOfficeSessionLimit;
private int pdfToHtmlSessionLimit;
private int pythonOpenCvSessionLimit;
private int weasyPrintSessionLimit;
private int installAppSessionLimit;
private int calibreSessionLimit;
private int qpdfSessionLimit;
private int tesseractSessionLimit;
public int getQpdfSessionLimit() {
return qpdfSessionLimit > 0 ? qpdfSessionLimit : 2;
}
public int getTesseractSessionLimit() {
return tesseractSessionLimit > 0 ? tesseractSessionLimit : 1;
}
public int getLibreOfficeSessionLimit() {
return libreOfficeSessionLimit > 0 ? libreOfficeSessionLimit : 1;
}
public int getPdfToHtmlSessionLimit() {
return pdfToHtmlSessionLimit > 0 ? pdfToHtmlSessionLimit : 1;
}
public int getPythonOpenCvSessionLimit() {
return pythonOpenCvSessionLimit > 0 ? pythonOpenCvSessionLimit : 8;
}
public int getWeasyPrintSessionLimit() {
return weasyPrintSessionLimit > 0 ? weasyPrintSessionLimit : 16;
}
public int getInstallAppSessionLimit() {
return installAppSessionLimit > 0 ? installAppSessionLimit : 1;
}
public int getCalibreSessionLimit() {
return calibreSessionLimit > 0 ? calibreSessionLimit : 1;
}
}
@Data
public static class TimeoutMinutes {
private long libreOfficeTimeoutMinutes;
private long pdfToHtmlTimeoutMinutes;
private long pythonOpenCvTimeoutMinutes;
private long weasyPrintTimeoutMinutes;
private long installAppTimeoutMinutes;
private long calibreTimeoutMinutes;
private long tesseractTimeoutMinutes;
private long qpdfTimeoutMinutes;
public long getTesseractTimeoutMinutes() {
return tesseractTimeoutMinutes > 0 ? tesseractTimeoutMinutes : 30;
}
public long getQpdfTimeoutMinutes() {
return qpdfTimeoutMinutes > 0 ? qpdfTimeoutMinutes : 30;
}
public long getLibreOfficeTimeoutMinutes() {
return libreOfficeTimeoutMinutes > 0 ? libreOfficeTimeoutMinutes : 30;
}
public long getPdfToHtmlTimeoutMinutes() {
return pdfToHtmlTimeoutMinutes > 0 ? pdfToHtmlTimeoutMinutes : 20;
}
public long getPythonOpenCvTimeoutMinutes() {
return pythonOpenCvTimeoutMinutes > 0 ? pythonOpenCvTimeoutMinutes : 30;
}
public long getWeasyPrintTimeoutMinutes() {
return weasyPrintTimeoutMinutes > 0 ? weasyPrintTimeoutMinutes : 30;
}
public long getInstallAppTimeoutMinutes() {
return installAppTimeoutMinutes > 0 ? installAppTimeoutMinutes : 60;
}
public long getCalibreTimeoutMinutes() {
return calibreTimeoutMinutes > 0 ? calibreTimeoutMinutes : 30;
}
}
}
}

View File

@@ -2,5 +2,5 @@ package stirling.software.SPDF.model;
public enum AuthenticationType {
WEB,
OAUTH2
SSO
}

View File

@@ -18,4 +18,15 @@ public class OptimizePdfRequest extends PDFFile {
@Schema(description = "The expected output size, e.g. '100MB', '25KB', etc.")
private String expectedOutputSize;
@Schema(
description = "Whether to linearize the PDF for faster web viewing. Default is false.",
defaultValue = "false")
private Boolean linearize = false;
@Schema(
description =
"Whether to normalize the PDF content for better compatibility. Default is true.",
defaultValue = "true")
private Boolean normalize = true;
}

View File

@@ -15,18 +15,6 @@ public class ProcessPdfWithOcrRequest extends PDFFile {
@Schema(description = "List of languages to use in OCR processing")
private List<String> languages;
@Schema(description = "Include OCR text in a sidecar text file if set to true")
private boolean sidecar;
@Schema(description = "Deskew the input file if set to true")
private boolean deskew;
@Schema(description = "Clean the input file if set to true")
private boolean clean;
@Schema(description = "Clean the final output if set to true")
private boolean cleanFinal;
@Schema(
description = "Specify the OCR type, e.g., 'skip-text', 'force-ocr', or 'Normal'",
allowableValues = {"skip-text", "force-ocr", "Normal"})
@@ -37,7 +25,4 @@ public class ProcessPdfWithOcrRequest extends PDFFile {
allowableValues = {"hocr", "sandwich"},
defaultValue = "hocr")
private String ocrRenderType = "hocr";
@Schema(description = "Remove images from the output PDF if set to true")
private boolean removeImagesAfter;
}

View File

@@ -50,4 +50,7 @@ public class SignPDFWithCertRequest extends PDFFile {
description =
"The page number where the signature should be visible. This is required if showSignature is set to true")
private Integer pageNumber;
@Schema(description = "Whether to visually show a signature logo along with the signature")
private boolean showLogo;
}

View File

@@ -0,0 +1,17 @@
package stirling.software.SPDF.model.api.security;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import stirling.software.SPDF.model.api.PDFFile;
@Data
@EqualsAndHashCode(callSuper = true)
public class SignatureValidationRequest extends PDFFile {
@Schema(description = "(Optional) file to compare PDF cert signatures against x.509 format")
private MultipartFile certFile;
}

View File

@@ -0,0 +1,31 @@
package stirling.software.SPDF.model.api.security;
import java.util.List;
import lombok.Data;
@Data
public class SignatureValidationResult {
private boolean valid;
private String signerName;
private String signatureDate;
private String reason;
private String location;
private String errorMessage;
private boolean chainValid;
private boolean trustValid;
private boolean notExpired;
private boolean notRevoked;
private boolean validAtTimeOfSigning;
private String issuerDN; // Certificate issuer's Distinguished Name
private String subjectDN; // Certificate subject's Distinguished Name
private String serialNumber; // Certificate serial number
private String validFrom; // Certificate validity start date
private String validUntil; // Certificate validity end date
private String signatureAlgorithm; // Algorithm used for signing
private int keySize; // Key size in bits
private String version; // Certificate version
private List<String> keyUsages; // List of key usage purposes
private boolean isSelfSigned; // Whether the certificate is self-signed
}

View File

@@ -0,0 +1,16 @@
package stirling.software.SPDF.pdf;
import org.apache.commons.csv.CSVFormat;
import technology.tabula.writers.CSVWriter;
public class FlexibleCSVWriter extends CSVWriter {
public FlexibleCSVWriter() {
super();
}
public FlexibleCSVWriter(CSVFormat csvFormat) {
super(csvFormat);
}
}

View File

@@ -5,6 +5,7 @@ import java.util.Date;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.web.authentication.rememberme.PersistentRememberMeToken;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.transaction.annotation.Transactional;
import stirling.software.SPDF.model.PersistentLogin;
@@ -13,6 +14,7 @@ public class JPATokenRepositoryImpl implements PersistentTokenRepository {
@Autowired private PersistentLoginRepository persistentLoginRepository;
@Override
@Transactional
public void createNewToken(PersistentRememberMeToken token) {
PersistentLogin newToken = new PersistentLogin();
newToken.setSeries(token.getSeries());
@@ -23,6 +25,7 @@ public class JPATokenRepositoryImpl implements PersistentTokenRepository {
}
@Override
@Transactional
public void updateToken(String series, String tokenValue, Date lastUsed) {
PersistentLogin existingToken = persistentLoginRepository.findById(series).orElse(null);
if (existingToken != null) {
@@ -43,11 +46,11 @@ public class JPATokenRepositoryImpl implements PersistentTokenRepository {
}
@Override
@Transactional
public void removeUserTokens(String username) {
for (PersistentLogin token : persistentLoginRepository.findAll()) {
if (token.getUsername().equals(username)) {
persistentLoginRepository.delete(token);
}
try {
persistentLoginRepository.deleteByUsername(username);
} catch (Exception e) {
}
}
}

View File

@@ -6,4 +6,6 @@ import org.springframework.stereotype.Repository;
import stirling.software.SPDF.model.PersistentLogin;
@Repository
public interface PersistentLoginRepository extends JpaRepository<PersistentLogin, String> {}
public interface PersistentLoginRepository extends JpaRepository<PersistentLogin, String> {
void deleteByUsername(String username);
}

View File

@@ -1,5 +1,6 @@
package stirling.software.SPDF.repository;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
@@ -19,4 +20,6 @@ public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username);
Optional<User> findByApiKey(String apiKey);
List<User> findByAuthenticationTypeIgnoreCase(String authenticationType);
}

View File

@@ -0,0 +1,260 @@
package stirling.software.SPDF.service;
import java.io.*;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.cert.*;
import java.security.cert.CertPath;
import java.security.cert.CertPathValidator;
import java.security.cert.CertificateExpiredException;
import java.security.cert.CertificateFactory;
import java.security.cert.CertificateNotYetValidException;
import java.security.cert.PKIXParameters;
import java.security.cert.TrustAnchor;
import java.security.cert.X509Certificate;
import java.util.*;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDEmbeddedFilesNameTreeNode;
import org.apache.pdfbox.pdmodel.common.filespecification.PDComplexFileSpecification;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Service;
import jakarta.annotation.PostConstruct;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
@Service
@Slf4j
public class CertificateValidationService {
private KeyStore trustStore;
private static final String AATL_RESOURCE = "/tl12.acrobatsecuritysettings";
@PostConstruct
private void initializeTrustStore() throws Exception {
trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
trustStore.load(null, null);
loadAATLCertificatesFromPDF();
}
private void loadAATLCertificatesFromPDF() throws Exception {
log.debug("Starting AATL certificate loading from PDF...");
try (InputStream pdfStream = new ClassPathResource(AATL_RESOURCE).getInputStream()) {
PDDocument document = Loader.loadPDF(pdfStream.readAllBytes());
PDEmbeddedFilesNameTreeNode embeddedFiles =
document.getDocumentCatalog().getNames().getEmbeddedFiles();
Map<String, PDComplexFileSpecification> files = embeddedFiles.getNames();
for (Map.Entry<String, PDComplexFileSpecification> entry : files.entrySet()) {
log.debug(entry.getKey());
if (entry.getKey().equals("SecuritySettings.xml")) {
byte[] xmlContent = entry.getValue().getEmbeddedFile().toByteArray();
processSecuritySettingsXML(xmlContent);
break;
}
}
}
}
private void processSecuritySettingsXML(byte[] xmlContent) throws Exception {
// Simple XML parsing using String operations
String xmlString = new String(xmlContent, "UTF-8");
int certCount = 0;
int failedCerts = 0;
// Find all Certificate tags
String startTag = "<Certificate>";
String endTag = "</Certificate>";
int startIndex = 0;
while ((startIndex = xmlString.indexOf(startTag, startIndex)) != -1) {
int endIndex = xmlString.indexOf(endTag, startIndex);
if (endIndex == -1) break;
// Extract certificate data
String certData = xmlString.substring(startIndex + startTag.length(), endIndex).trim();
startIndex = endIndex + endTag.length();
try {
byte[] certBytes = Base64.getDecoder().decode(certData);
CertificateFactory cf = CertificateFactory.getInstance("X.509");
X509Certificate cert =
(X509Certificate)
cf.generateCertificate(new ByteArrayInputStream(certBytes));
// Only store root certificates (self-signed)
if (cert.getSubjectX500Principal().equals(cert.getIssuerX500Principal())) {
trustStore.setCertificateEntry("aatl-cert-" + certCount, cert);
log.trace(
"Successfully loaded AATL root certificate #"
+ certCount
+ "\n Subject: "
+ cert.getSubjectX500Principal().getName()
+ "\n Valid until: "
+ cert.getNotAfter());
certCount++;
}
} catch (Exception e) {
failedCerts++;
log.error("Failed to process AATL certificate: " + e.getMessage());
}
}
log.debug("AATL Certificate loading completed:");
log.debug(" Total root certificates successfully loaded: " + certCount);
log.debug(" Failed certificates: " + failedCerts);
}
@Data
public static class ValidationResult {
private boolean valid;
private boolean expired;
private boolean validAtSigningTime;
private String errorMessage;
}
public ValidationResult validateCertificateChain(X509Certificate signerCert) {
ValidationResult result = new ValidationResult();
try {
// Build the certificate chain
List<X509Certificate> certChain = buildCertificateChain(signerCert);
// Create certificate path
CertificateFactory cf = CertificateFactory.getInstance("X.509");
CertPath certPath = cf.generateCertPath(certChain);
// Set up trust anchors
Set<TrustAnchor> anchors = new HashSet<>();
Enumeration<String> aliases = trustStore.aliases();
while (aliases.hasMoreElements()) {
Object trustCert = trustStore.getCertificate(aliases.nextElement());
if (trustCert instanceof X509Certificate) {
anchors.add(new TrustAnchor((X509Certificate) trustCert, null));
}
}
// Set up validation parameters
PKIXParameters params = new PKIXParameters(anchors);
params.setRevocationEnabled(false);
// Validate the path
CertPathValidator validator = CertPathValidator.getInstance("PKIX");
validator.validate(certPath, params);
result.setValid(true);
result.setExpired(isExpired(signerCert));
return result;
} catch (Exception e) {
result.setValid(false);
result.setErrorMessage(e.getMessage());
return result;
}
}
public ValidationResult validateWithCustomCert(
X509Certificate signerCert, X509Certificate customCert) {
ValidationResult result = new ValidationResult();
try {
// Build the complete chain from signer cert
List<X509Certificate> certChain = buildCertificateChain(signerCert);
// Check if custom cert matches any cert in the chain
boolean matchFound = false;
for (X509Certificate chainCert : certChain) {
if (chainCert.equals(customCert)) {
matchFound = true;
break;
}
}
if (!matchFound) {
// Check if custom cert is a valid issuer for any cert in the chain
for (X509Certificate chainCert : certChain) {
try {
chainCert.verify(customCert.getPublicKey());
matchFound = true;
break;
} catch (Exception e) {
// Continue checking next cert
}
}
}
result.setValid(matchFound);
if (!matchFound) {
result.setErrorMessage(
"Custom certificate is not part of the chain and is not a valid issuer");
}
return result;
} catch (Exception e) {
result.setValid(false);
result.setErrorMessage(e.getMessage());
return result;
}
}
private List<X509Certificate> buildCertificateChain(X509Certificate signerCert)
throws CertificateException {
List<X509Certificate> chain = new ArrayList<>();
chain.add(signerCert);
X509Certificate current = signerCert;
while (!isSelfSigned(current)) {
X509Certificate issuer = findIssuer(current);
if (issuer == null) break;
chain.add(issuer);
current = issuer;
}
return chain;
}
private boolean isSelfSigned(X509Certificate cert) {
return cert.getSubjectX500Principal().equals(cert.getIssuerX500Principal());
}
private X509Certificate findIssuer(X509Certificate cert) throws CertificateException {
try {
Enumeration<String> aliases = trustStore.aliases();
while (aliases.hasMoreElements()) {
Certificate trustCert = trustStore.getCertificate(aliases.nextElement());
if (trustCert instanceof X509Certificate) {
X509Certificate x509TrustCert = (X509Certificate) trustCert;
if (cert.getIssuerX500Principal()
.equals(x509TrustCert.getSubjectX500Principal())) {
try {
cert.verify(x509TrustCert.getPublicKey());
return x509TrustCert;
} catch (Exception e) {
// Continue searching if verification fails
}
}
}
}
} catch (KeyStoreException e) {
throw new CertificateException("Error accessing trust store", e);
}
return null;
}
private boolean isExpired(X509Certificate cert) {
try {
cert.checkValidity();
return false;
} catch (CertificateExpiredException | CertificateNotYetValidException e) {
return true;
}
}
}

View File

@@ -0,0 +1,41 @@
package stirling.software.SPDF.service;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.stereotype.Service;
@Service
public class LanguageService {
private final PathMatchingResourcePatternResolver resourcePatternResolver =
new PathMatchingResourcePatternResolver();
public List<String> getSupportedLanguages() {
List<String> supportedLanguages = new ArrayList<>();
try {
Resource[] resources =
resourcePatternResolver.getResources("classpath*:messages_*.properties");
for (Resource resource : resources) {
if (resource.exists() && resource.isReadable()) {
String filename = resource.getFilename();
if (filename != null
&& filename.startsWith("messages_")
&& filename.endsWith(".properties")) {
String languageCode =
filename.replace("messages_", "").replace(".properties", "");
supportedLanguages.add(languageCode);
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
return supportedLanguages;
}
}

View File

@@ -24,7 +24,7 @@ public class MetricsAggregatorService {
this.postHogService = postHogService;
}
@Scheduled(fixedRate = 900000) // Run every 15 minutes
@Scheduled(fixedRate = 7200000) // Run every 2 hours
public void aggregateAndSendMetrics() {
Map<String, Object> metrics = new HashMap<>();
Search.in(meterRegistry)
@@ -32,11 +32,24 @@ public class MetricsAggregatorService {
.counters()
.forEach(
counter -> {
String method = counter.getId().getTag("method");
String uri = counter.getId().getTag("uri");
// Skip if either method or uri is null
if (method == null || uri == null) {
return;
}
if (!method.equals("GET") && !method.equals("POST")) {
return;
}
// Skip URIs that are 2 characters or shorter
if (uri.length() <= 2) {
return;
}
String key =
String.format(
"http_requests_%s_%s",
counter.getId().getTag("method"),
counter.getId().getTag("uri").replace("/", "_"));
"http_requests_%s_%s", method, uri.replace("/", "_"));
double currentCount = counter.count();
double lastCount = lastSentMetrics.getOrDefault(key, 0.0);

View File

@@ -17,15 +17,18 @@ public class PdfMetadataService {
private final ApplicationProperties applicationProperties;
private final String stirlingPDFLabel;
private final UserServiceInterface userService;
private final boolean runningEE;
@Autowired
public PdfMetadataService(
ApplicationProperties applicationProperties,
@Qualifier("StirlingPDFLabel") String stirlingPDFLabel,
@Qualifier("runningEE") boolean runningEE,
@Autowired(required = false) UserServiceInterface userService) {
this.applicationProperties = applicationProperties;
this.stirlingPDFLabel = stirlingPDFLabel;
this.userService = userService;
this.runningEE = runningEE;
}
public PdfMetadata extractMetadataFromPdf(PDDocument pdf) {
@@ -61,10 +64,8 @@ public class PdfMetadataService {
String creator = stirlingPDFLabel;
if (applicationProperties
.getEnterpriseEdition()
.getCustomMetadata()
.isAutoUpdateMetadata()) {
if (applicationProperties.getEnterpriseEdition().getCustomMetadata().isAutoUpdateMetadata()
&& runningEE) {
creator = applicationProperties.getEnterpriseEdition().getCustomMetadata().getCreator();
pdf.getDocumentInformation().setProducer(stirlingPDFLabel);
@@ -83,10 +84,8 @@ public class PdfMetadataService {
pdf.getDocumentInformation().setModificationDate(Calendar.getInstance());
String author = pdfMetadata.getAuthor();
if (applicationProperties
.getEnterpriseEdition()
.getCustomMetadata()
.isAutoUpdateMetadata()) {
if (applicationProperties.getEnterpriseEdition().getCustomMetadata().isAutoUpdateMetadata()
&& runningEE) {
author = applicationProperties.getEnterpriseEdition().getCustomMetadata().getAuthor();
if (userService != null) {

View File

@@ -15,6 +15,7 @@ import java.util.TimeZone;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Service;
import com.posthog.java.PostHog;
@@ -26,19 +27,25 @@ import stirling.software.SPDF.model.ApplicationProperties;
public class PostHogService {
private final PostHog postHog;
private final String uniqueId;
private final String appVersion;
private final ApplicationProperties applicationProperties;
private final UserServiceInterface userService;
private final Environment env;
@Autowired
public PostHogService(
PostHog postHog,
@Qualifier("UUID") String uuid,
@Qualifier("appVersion") String appVersion,
ApplicationProperties applicationProperties,
@Autowired(required = false) UserServiceInterface userService) {
@Autowired(required = false) UserServiceInterface userService,
Environment env) {
this.postHog = postHog;
this.uniqueId = uuid;
this.appVersion = appVersion;
this.applicationProperties = applicationProperties;
this.userService = userService;
this.env = env;
captureSystemInfo();
}
@@ -64,6 +71,16 @@ public class PostHogService {
Map<String, Object> metrics = new HashMap<>();
try {
// Application version
metrics.put("app_version", appVersion);
String deploymentType = "JAR"; // default
if ("true".equalsIgnoreCase(env.getProperty("BROWSER_OPEN"))) {
deploymentType = "EXE";
} else if (isRunningInDocker()) {
deploymentType = "DOCKER";
}
metrics.put("deployment_type", deploymentType);
// System info
metrics.put("os_name", System.getProperty("os.name"));
metrics.put("os_version", System.getProperty("os.version"));
@@ -132,7 +149,6 @@ public class PostHogService {
// Docker detection and stats
boolean isDocker = isRunningInDocker();
metrics.put("is_docker", isDocker);
if (isDocker) {
metrics.put("docker_metrics", getDockerMetrics());
}

View File

@@ -105,7 +105,7 @@ public class FileToPdf {
new ByteArrayInputStream(Files.readAllBytes(zipFilePath)))) {
ZipEntry entry = zipIn.getNextEntry();
while (entry != null) {
Path filePath = tempUnzippedDir.resolve(entry.getName());
Path filePath = tempUnzippedDir.resolve(sanitizeZipFilename(entry.getName()));
if (!entry.isDirectory()) {
Files.createDirectories(filePath.getParent());
if (entry.getName().toLowerCase().endsWith(".html")
@@ -175,7 +175,7 @@ public class FileToPdf {
ZipSecurity.createHardenedInputStream(new ByteArrayInputStream(fileBytes))) {
ZipEntry entry = zipIn.getNextEntry();
while (entry != null) {
Path filePath = tempDirectory.resolve(entry.getName());
Path filePath = tempDirectory.resolve(sanitizeZipFilename(entry.getName()));
if (entry.isDirectory()) {
Files.createDirectories(filePath); // Explicitly create the directory structure
} else {
@@ -241,4 +241,14 @@ public class FileToPdf {
Files.deleteIfExists(tempOutputFile);
}
}
static String sanitizeZipFilename(String entryName) {
if (entryName == null || entryName.trim().isEmpty()) {
return entryName;
}
while (entryName.contains("../") || entryName.contains("..\\")) {
entryName = entryName.replace("../", "").replace("..\\", "");
}
return entryName;
}
}

View File

@@ -18,19 +18,23 @@ import org.slf4j.LoggerFactory;
import io.github.pixee.security.BoundedLineReader;
import stirling.software.SPDF.model.ApplicationProperties;
public class ProcessExecutor {
private static final Logger logger = LoggerFactory.getLogger(ProcessExecutor.class);
private static ApplicationProperties applicationProperties = new ApplicationProperties();
public enum Processes {
LIBRE_OFFICE,
PDFTOHTML,
OCR_MY_PDF,
PYTHON_OPENCV,
GHOSTSCRIPT,
WEASYPRINT,
INSTALL_APP,
CALIBRE
CALIBRE,
TESSERACT,
QPDF
}
private static final Map<Processes, ProcessExecutor> instances = new ConcurrentHashMap<>();
@@ -45,26 +49,90 @@ public class ProcessExecutor {
key -> {
int semaphoreLimit =
switch (key) {
case LIBRE_OFFICE -> 1;
case PDFTOHTML -> 1;
case OCR_MY_PDF -> 2;
case PYTHON_OPENCV -> 8;
case GHOSTSCRIPT -> 16;
case WEASYPRINT -> 16;
case INSTALL_APP -> 1;
case CALIBRE -> 1;
case LIBRE_OFFICE ->
applicationProperties
.getProcessExecutor()
.getSessionLimit()
.getLibreOfficeSessionLimit();
case PDFTOHTML ->
applicationProperties
.getProcessExecutor()
.getSessionLimit()
.getPdfToHtmlSessionLimit();
case PYTHON_OPENCV ->
applicationProperties
.getProcessExecutor()
.getSessionLimit()
.getPythonOpenCvSessionLimit();
case WEASYPRINT ->
applicationProperties
.getProcessExecutor()
.getSessionLimit()
.getWeasyPrintSessionLimit();
case INSTALL_APP ->
applicationProperties
.getProcessExecutor()
.getSessionLimit()
.getInstallAppSessionLimit();
case TESSERACT ->
applicationProperties
.getProcessExecutor()
.getSessionLimit()
.getTesseractSessionLimit();
case QPDF ->
applicationProperties
.getProcessExecutor()
.getSessionLimit()
.getQpdfSessionLimit();
case CALIBRE ->
applicationProperties
.getProcessExecutor()
.getSessionLimit()
.getCalibreSessionLimit();
};
long timeoutMinutes =
switch (key) {
case LIBRE_OFFICE -> 30;
case PDFTOHTML -> 20;
case OCR_MY_PDF -> 30;
case PYTHON_OPENCV -> 30;
case GHOSTSCRIPT -> 30;
case WEASYPRINT -> 30;
case INSTALL_APP -> 60;
case CALIBRE -> 30;
case LIBRE_OFFICE ->
applicationProperties
.getProcessExecutor()
.getTimeoutMinutes()
.getLibreOfficeTimeoutMinutes();
case PDFTOHTML ->
applicationProperties
.getProcessExecutor()
.getTimeoutMinutes()
.getPdfToHtmlTimeoutMinutes();
case PYTHON_OPENCV ->
applicationProperties
.getProcessExecutor()
.getTimeoutMinutes()
.getPythonOpenCvTimeoutMinutes();
case WEASYPRINT ->
applicationProperties
.getProcessExecutor()
.getTimeoutMinutes()
.getWeasyPrintTimeoutMinutes();
case INSTALL_APP ->
applicationProperties
.getProcessExecutor()
.getTimeoutMinutes()
.getInstallAppTimeoutMinutes();
case TESSERACT ->
applicationProperties
.getProcessExecutor()
.getTimeoutMinutes()
.getTesseractTimeoutMinutes();
case QPDF ->
applicationProperties
.getProcessExecutor()
.getTimeoutMinutes()
.getQpdfTimeoutMinutes();
case CALIBRE ->
applicationProperties
.getProcessExecutor()
.getTimeoutMinutes()
.getCalibreTimeoutMinutes();
};
return new ProcessExecutor(semaphoreLimit, liveUpdates, timeoutMinutes);
});

View File

@@ -0,0 +1,26 @@
package stirling.software.SPDF.utils.propertyeditor;
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;
public class StringToMapPropertyEditor extends PropertyEditorSupport {
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
public void setAsText(String text) throws IllegalArgumentException {
try {
TypeReference<HashMap<String, String>> typeRef =
new TypeReference<HashMap<String, String>>() {};
Map<String, String> map = objectMapper.readValue(text, typeRef);
setValue(map);
} catch (Exception e) {
throw new IllegalArgumentException(
"Failed to convert java.lang.String to java.util.Map");
}
}
}

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