Compare commits

...

115 Commits

Author SHA1 Message Date
github-actions[bot]
5564a6e730 💾 Update Version (#1727)
💾 Sync Versions
> Made via sync_files.yml

Co-authored-by: GitHub Action action@github.com <GitHub Action action@github.com>
2024-08-20 17:49:33 +01:00
Anthony Stirling
8a58647ffd Update build.gradle 2024-08-20 17:49:10 +01:00
Anthony Stirling
37dcae282a ExtractImagesController. null checks 2024-08-20 17:20:18 +01:00
Ludy
58618b3a21 Add: Convert PDF to WebP (#1666)
* Add PDF to WebP

* add swagger param

* back

* creates a custom image for Docker from pymupdf

* Converting with pdf2image and Pillow instead of pymupdf

* webp remove to pdf-to-img

* remove mupdf
2024-08-20 16:17:54 +01:00
github-actions[bot]
4a4c7faf47 📝 Update README: Translation Progress Table (#1725)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: GitHub Action action@github.com <GitHub Action action@github.com>
2024-08-20 10:59:08 +01:00
github-actions[bot]
66324a5bdc Update 3rd Party Licenses (#1724)
Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: GitHub Action <action@github.com>
2024-08-20 10:57:53 +01:00
Guilherme L. Leite Marques
3bd18f7c5e Updated pt_BR ignored translations; Improved pt_BR translation (#1705)
* Updated pt_BR ignored translations

* Fixed blank lines
2024-08-20 10:57:42 +01:00
dependabot[bot]
e693bbb2bd Bump com.bucket4j:bucket4j_jdk17-core from 8.13.1 to 8.14.0 (#1720)
Bumps [com.bucket4j:bucket4j_jdk17-core](https://github.com/bucket4j/bucket4j) from 8.13.1 to 8.14.0.
- [Release notes](https://github.com/bucket4j/bucket4j/releases)
- [Commits](https://github.com/bucket4j/bucket4j/commits/8.14.0)

---
updated-dependencies:
- dependency-name: com.bucket4j:bucket4j_jdk17-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-08-20 10:55:00 +01:00
Црнобог
f11ad92fa5 Typing error in README.md (#1721)
Misstype |  Sebian => Serbian
2024-08-20 10:54:44 +01:00
Anthony Stirling
f443a4e0de reduce google font size (#1723)
* Delete src/main/resources/static/fonts/google-symbol.woff2

* Add files via upload
2024-08-20 09:57:12 +01:00
PingLin8888
fa0152aa2d Fix ConcurrentModificationException by modifying resources outside the iteration. (#1719)
Fix ConcurrentModificationException by collecting XObject names

- Changed  to use a list to collect XObject names before removal.
- Avoids ConcurrentModificationException by modifying resources outside the iteration.

Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2024-08-19 19:42:55 +01:00
Ludy
e1d0f2cd3e Fix: YamlFile - String length limit disable (#1716)
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2024-08-19 15:03:05 +01:00
Ludy
81e2a77e57 Fix: Failed authentication #1704 (#1708)
* Fix: Failed authentication #1704

* Update account.html
2024-08-19 15:02:40 +01:00
Ludy
6c9a4e8acc Add Option to Specify Installation Method in Bug Report Template (#1710)
* Add Option to Specify Installation Method in Bug Report Template

* Update 1-bug.yml
2024-08-19 15:00:23 +01:00
albanobattistella
8e5b3ea7f1 Update messages_it_IT.properties (#1713) 2024-08-19 15:00:12 +01:00
Ludy
b47f8a2c17 corrects the link to the country selection (#1717) 2024-08-19 15:00:00 +01:00
Ludy
56a07bbf3a increases some versions in the workflows (#1707) 2024-08-18 13:07:14 +01:00
github-actions[bot]
987d793ef4 Update 3rd Party Licenses (#1701)
Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: GitHub Action <action@github.com>
2024-08-16 13:27:18 +01:00
github-actions[bot]
ea85d76f5b 💾 Update Version (#1700)
💾 Sync Versions
> Made via sync_files.yml

Co-authored-by: GitHub Action action@github.com <GitHub Action action@github.com>
2024-08-16 13:24:53 +01:00
Anthony Stirling
fc762329a8 Update build.gradle 2024-08-16 13:24:32 +01:00
github-actions[bot]
48bae227f6 💾 Update Version (#1699)
💾 Sync Versions
> Made via sync_files.yml

Co-authored-by: GitHub Action action@github.com <GitHub Action action@github.com>
2024-08-16 12:52:25 +01:00
Anthony Stirling
9773138612 Update build.gradle 2024-08-16 12:51:59 +01:00
github-actions[bot]
1927801894 Update 3rd Party Licenses (#1698)
Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: GitHub Action <action@github.com>
2024-08-16 12:47:46 +01:00
Anthony Stirling
3cd6f462a8 Update build.gradle to use commmon versions (#1697)
* Update build.gradle

* Update build.gradle
2024-08-16 12:44:25 +01:00
github-actions[bot]
6dab3980fc Update 3rd Party Licenses (#1696)
Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: GitHub Action <action@github.com>
2024-08-16 12:15:55 +01:00
Anthony Stirling
ea2d755808 Update licenses-update.yml 2024-08-16 12:15:07 +01:00
dependabot[bot]
05efcedea8 Bump ch.qos.logback:logback-classic from 1.5.6 to 1.5.7 (#1685)
* Bump ch.qos.logback:logback-classic from 1.5.6 to 1.5.7

Bumps [ch.qos.logback:logback-classic](https://github.com/qos-ch/logback) from 1.5.6 to 1.5.7.
- [Commits](https://github.com/qos-ch/logback/compare/v_1.5.6...v_1.5.7)

---
updated-dependencies:
- dependency-name: ch.qos.logback:logback-classic
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

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

* Update build.gradle

---------

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-08-16 12:03:59 +01:00
Ludy
29fcbf30d7 Admin panel - Enhanced User Management & Fix: #1630 (#1658)
* Prevents SSO login due to faulty verification

* add translation & fix show error message

* Update settings.yml.template

---------

Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2024-08-16 11:57:37 +01:00
github-actions[bot]
2cbe34ea24 Update 3rd Party Licenses (#1695)
Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: GitHub Action <action@github.com>
2024-08-16 11:52:51 +01:00
dependabot[bot]
29f43c010e Bump io.micrometer:micrometer-core from 1.13.0 to 1.13.3 (#1689)
Bumps [io.micrometer:micrometer-core](https://github.com/micrometer-metrics/micrometer) from 1.13.0 to 1.13.3.
- [Release notes](https://github.com/micrometer-metrics/micrometer/releases)
- [Commits](https://github.com/micrometer-metrics/micrometer/compare/v1.13.0...v1.13.3)

---
updated-dependencies:
- dependency-name: io.micrometer:micrometer-core
  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>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2024-08-16 11:51:40 +01:00
Anthony Stirling
8602f38fbf Update build.yml 2024-08-16 11:44:49 +01:00
Anthony Stirling
f5258c593b Update auto-labeler.yml 2024-08-16 11:15:37 +01:00
Anthony Stirling
e89ac84928 Update build.yml 2024-08-16 11:09:17 +01:00
Anthony Stirling
8997855922 Update auto-labeler.yml 2024-08-16 11:02:38 +01:00
Anthony Stirling
09c93cebe3 PR changes (#1693)
* Update licenses-update.yml

* Update build.yml

* Update test.yml

* Delete .github/workflows/test.yml

* Update build.yml

* Update build.yml

* Update auto-labeler.yml

* Update build.yml

* Update auto-labeler.yml

* Update labeler-config.yml
2024-08-16 11:00:10 +01:00
github-actions[bot]
00d65596d1 Update 3rd Party Licenses (#1687)
Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: GitHub Action <action@github.com>
2024-08-16 08:48:42 +01:00
dependabot[bot]
851b43fadf Bump com.github.jk1.dependency-license-report from 2.8 to 2.9 (#1690)
Bumps com.github.jk1.dependency-license-report from 2.8 to 2.9.

---
updated-dependencies:
- dependency-name: com.github.jk1.dependency-license-report
  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-08-16 00:04:43 +02:00
dependabot[bot]
d5ac560452 Bump org.apache.pdfbox:pdfbox from 3.0.2 to 3.0.3 (#1691)
Bumps org.apache.pdfbox:pdfbox from 3.0.2 to 3.0.3.

---
updated-dependencies:
- dependency-name: org.apache.pdfbox:pdfbox
  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-08-15 23:57:45 +02:00
dependabot[bot]
4ea323b879 Bump edu.sc.seis.launch4j from 3.0.5 to 3.0.6 (#1686)
Bumps edu.sc.seis.launch4j from 3.0.5 to 3.0.6.

---
updated-dependencies:
- dependency-name: edu.sc.seis.launch4j
  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-08-15 21:41:03 +00:00
dependabot[bot]
5c84ae1b53 Bump org.springframework:spring-webmvc from 6.1.9 to 6.1.12 (#1680)
Bumps [org.springframework:spring-webmvc](https://github.com/spring-projects/spring-framework) from 6.1.9 to 6.1.12.
- [Release notes](https://github.com/spring-projects/spring-framework/releases)
- [Commits](https://github.com/spring-projects/spring-framework/compare/v6.1.9...v6.1.12)

---
updated-dependencies:
- dependency-name: org.springframework:spring-webmvc
  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-08-15 21:36:26 +00:00
dependabot[bot]
96a8898c15 Bump com.bucket4j:bucket4j_jdk17-core from 8.12.1 to 8.13.1 (#1683)
Bumps [com.bucket4j:bucket4j_jdk17-core](https://github.com/bucket4j/bucket4j) from 8.12.1 to 8.13.1.
- [Release notes](https://github.com/bucket4j/bucket4j/releases)
- [Commits](https://github.com/bucket4j/bucket4j/commits)

---
updated-dependencies:
- dependency-name: com.bucket4j:bucket4j_jdk17-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-08-15 21:34:04 +00:00
dependabot[bot]
1cca13334e Bump org.projectlombok:lombok from 1.18.32 to 1.18.34 (#1684)
Bumps [org.projectlombok:lombok](https://github.com/projectlombok/lombok) from 1.18.32 to 1.18.34.
- [Changelog](https://github.com/projectlombok/lombok/blob/master/doc/changelog.markdown)
- [Commits](https://github.com/projectlombok/lombok/compare/v1.18.32...v1.18.34)

---
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-08-15 23:31:57 +02:00
dependabot[bot]
264763dd4c Bump io.github.pixee:java-security-toolkit from 1.1.3 to 1.2.0 (#1667)
Bumps [io.github.pixee:java-security-toolkit](https://github.com/pixee/java-security-toolkit) from 1.1.3 to 1.2.0.
- [Release notes](https://github.com/pixee/java-security-toolkit/releases)
- [Commits](https://github.com/pixee/java-security-toolkit/compare/v1.1.3...v1.2.0)

---
updated-dependencies:
- dependency-name: io.github.pixee:java-security-toolkit
  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-08-15 22:01:41 +01:00
dependabot[bot]
eafbfb8dbf Bump org.apache.pdfbox:xmpbox from 3.0.2 to 3.0.3 (#1670)
Bumps org.apache.pdfbox:xmpbox from 3.0.2 to 3.0.3.

---
updated-dependencies:
- dependency-name: org.apache.pdfbox:xmpbox
  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-08-15 22:01:03 +01:00
Anthony Stirling
6fa7c2e5e1 Update dependabot.yml 2024-08-15 22:00:02 +01:00
Anthony Stirling
909054a49d Update dependabot.yml 2024-08-15 21:57:37 +01:00
kmau
711501a382 Update messages_it_IT.properties - remove image in home (#1661) 2024-08-15 09:46:54 +00:00
github-actions[bot]
e45b512087 📝 Update README: Translation Progress Table (#1675)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: GitHub Action action@github.com <GitHub Action action@github.com>
2024-08-15 09:45:19 +00:00
Ludy
d32da95f55 tessdata available to local Windows users (#1677)
tessdata available to local Windows users
2024-08-15 11:43:56 +02:00
Rafael Martins
b54d73d723 Updated brazilian portuguese translation (#1673)
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2024-08-14 10:59:06 +00:00
Ludy
503a1c9526 redesign footer (#1674)
* redesign footer

* remove about test
2024-08-13 23:54:33 +02:00
Ludy
f176558a39 Fix: Conditional Attribute Binding for the multiple Attribute in the File Selector Fragment (#1665) 2024-08-12 17:22:32 +01:00
github-actions[bot]
68c387086c 📝 Update README: Translation Progress Table (#1655)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: GitHub Action action@github.com <GitHub Action action@github.com>
2024-08-09 08:58:31 +01:00
Anthony Stirling
f165439d26 Update remove-pages.html
#1656
2024-08-09 08:57:29 +01:00
tkymmm
6649ffd7a0 Updated Japanese translation (#1654)
* Update messages_ja_JP.properties

Updated Japanese translation

* Update messages_ja_JP.properties

Updated Japanese translation
2024-08-09 07:39:35 +01:00
github-actions[bot]
8dbbacb09e 📝 Update README: Translation Progress Table (#1651)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: GitHub Action action@github.com <GitHub Action action@github.com>
2024-08-08 22:29:02 +01:00
albanobattistella
908b409155 Update messages_it_IT.properties (#1649) 2024-08-08 22:06:13 +01:00
github-actions[bot]
4ad716f281 Update 3rd Party Licenses (#1648)
Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: GitHub Action <action@github.com>
2024-08-08 21:41:40 +01:00
Diallo
148feda83f Bug fix UI crash when url is unrechable (#1642)
* feat: Add URL  reachability check in ConvertWebsiteToPDF

* Add tests for URL reachability in ConvertWebsiteToPdfTest

* test: Update URL in ConvertWebsiteToPdfTest for testing
2024-08-08 20:35:15 +00:00
dependabot[bot]
771b312ee8 Bump com.twelvemonkeys.imageio:imageio-tiff from 3.10.1 to 3.11.0 (#1503)
* Bump com.twelvemonkeys.imageio:imageio-tiff from 3.10.1 to 3.11.0

Bumps com.twelvemonkeys.imageio:imageio-tiff from 3.10.1 to 3.11.0.

---
updated-dependencies:
- dependency-name: com.twelvemonkeys.imageio:imageio-tiff
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

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

* Update build.gradle

---------

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-08-08 21:33:14 +01:00
github-actions[bot]
00a0670954 📝 Update README: Translation Progress Table (#1647)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: GitHub Action action@github.com <GitHub Action action@github.com>
2024-08-08 21:17:18 +01:00
github-actions[bot]
39423c247c 💾 Update Version (#1646)
💾 Sync Versions
> Made via sync_files.yml

Co-authored-by: GitHub Action action@github.com <GitHub Action action@github.com>
2024-08-08 21:17:08 +01:00
Anthony Stirling
6d8d0bad56 langs 2024-08-08 21:15:41 +01:00
Anthony Stirling
a3374745f8 formatting 2024-08-08 21:13:59 +01:00
dependabot[bot]
d65a637a46 Bump alpine from 3.20.0 to 3.20.1 (#1505)
* Bump alpine from 3.20.0 to 3.20.1

Bumps alpine from 3.20.0 to 3.20.1.

---
updated-dependencies:
- dependency-name: alpine
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

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

* Update Dockerfile

* Update Dockerfile-fat

* Update Dockerfile-ultra-lite

---------

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-08-08 20:49:56 +01:00
PingLin8888
d0bf385d69 Issue1632 remove images (#1645)
* Implemented PdfImageRemovalService.java and PdfImageRemovalController.java. Image can be removed testing using Postman, but the file size doesn't change.

* Fix removal logic in service file to decrease file size.

* Implement "Remove Image" feature on the website

Updated the front-end code to integrate the "Remove Image" feature. The new functionality is now fully operational on the website, allowing users to remove images as expected.

* Add comments to PdfImageRemovalController and PdfImageRemovalService.

* Change the google material icon in navbar, homepage and remove-image-pdf.html.
2024-08-08 20:38:36 +01:00
github-actions[bot]
bc35745768 📝 Update README: Translation Progress Table (#1640)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: GitHub Action action@github.com <GitHub Action action@github.com>
2024-08-07 22:19:08 +01:00
HimaGirija
e50391a44a Added multithreaded feature for image extraction (#1641)
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2024-08-07 22:16:57 +01:00
arsvendg
96b080528b Changes norwegian translation (#1639)
* Minor correction

* Endringer oversettelser

* Changes norwegian translation
2024-08-07 09:44:44 +01:00
Anthony Stirling
f35cbc4310 enhancement auto have label 2024-08-06 10:57:18 +01:00
github-actions[bot]
c09fc1541f 📝 Update README: Translation Progress Table (#1636)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: GitHub Action action@github.com <GitHub Action action@github.com>
2024-08-06 10:51:50 +01:00
Anthony Stirling
dff53310a7 lang 2024-08-06 10:50:47 +01:00
mylk13
ec537c6fde Add a checkbox to WatermarkController to convert the pdf to pdf-image (#1633)
* Add a checkbox to WatermarkController to convert the pdf to pdf-image

* 381: Fix messages_en_GB

---------

Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2024-08-06 08:11:52 +00:00
dependabot[bot]
ce70796fff Bump org.mockito:mockito-inline from 3.12.4 to 5.2.0 (#1635)
Bumps [org.mockito:mockito-inline](https://github.com/mockito/mockito) from 3.12.4 to 5.2.0.
- [Release notes](https://github.com/mockito/mockito/releases)
- [Commits](https://github.com/mockito/mockito/compare/v3.12.4...v5.2.0)

---
updated-dependencies:
- dependency-name: org.mockito:mockito-inline
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-06 09:10:14 +01:00
Mateusz Tylec
7db7192d95 Update polish translation (#1631) 2024-08-04 21:26:55 +01:00
github-actions[bot]
d00e7fe958 Update 3rd Party Licenses (#1627)
Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: GitHub Action <action@github.com>
2024-08-03 13:47:59 +01:00
Anthony Stirling
510f39ad41 Update examples.feature 2024-08-03 13:47:47 +01:00
Ludy
950a0c4b21 Bump org.springframework.boot from 3.3.0 to 3.3.2 & Gradle 8 compatibility (#1626)
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2024-08-03 13:18:51 +01:00
Ludy
e6793bd04a Fix: fail JUnit test (#1625) 2024-08-03 12:52:50 +01:00
Anthony Stirling
0f60974a57 Update examples.feature (#1624) 2024-08-03 10:44:17 +01:00
dependabot[bot]
0ed4c16dc0 Bump io.spring.dependency-management from 1.1.5 to 1.1.6 (#1579)
Bumps [io.spring.dependency-management](https://github.com/spring-gradle-plugins/dependency-management-plugin) from 1.1.5 to 1.1.6.
- [Release notes](https://github.com/spring-gradle-plugins/dependency-management-plugin/releases)
- [Commits](https://github.com/spring-gradle-plugins/dependency-management-plugin/compare/v1.1.5...v1.1.6)

---
updated-dependencies:
- dependency-name: io.spring.dependency-management
  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-08-03 10:33:57 +01:00
Manohar Mannam
ea6d4a293e blank pages returns removed pages for verification #1574 (#1619)
separated blank and non-blank pages and created unified ZIP archive

Co-authored-by: mannam <101550345+ManoharMannam@users.noreply.github.com>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2024-08-03 09:30:53 +00:00
Anthony Stirling
191e79da18 Update test.yml (#1623)
* Update test.yml

* Update SPdfApplication.java
2024-08-03 10:29:34 +01:00
Ludy
c54c18b247 Add: Irish and Danish to the table (#1622) 2024-08-03 10:16:26 +01:00
github-actions[bot]
39cbb5e7d9 📝 Update README: Translation Progress Table (#1615)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: GitHub Action action@github.com <GitHub Action action@github.com>
2024-08-01 00:10:12 +01:00
LizardWizardGB
3df0474ed2 Danish language (#1606)
* Update languages.html

Added an entry for "Danish".

* Update languages.html

filename of flag was wrong.

* Danish flag svg

* Create messages_da_DK.properties

Initial commit of danish translation.

* Update messages_da_DK.properties

* Update messages_da_DK.properties
2024-07-31 21:25:57 +01:00
congyuluo
9ff2cb63d0 Refactored Identifiers (#1609) 2024-07-31 21:25:48 +01:00
github-actions[bot]
d8087d8c55 📝 Update README: Translation Progress Table (#1613)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: GitHub Action action@github.com <GitHub Action action@github.com>
2024-07-31 21:25:37 +01:00
Aindriú Mac Giolla Eoin
0dfb4d77c0 Added Irish Language (#1607)
Adding Irish Language
2024-07-31 18:17:01 +00:00
Ludy
065f53e577 Optimize Editor and Git Ignore Settings for Improved Consistency and Security (#1611) 2024-07-31 18:49:52 +01:00
github-actions[bot]
c899f605a9 📝 Update README: Translation Progress Table (#1601)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: GitHub Action action@github.com <GitHub Action action@github.com>
2024-07-27 09:11:41 +01:00
DeH40
47de0f84db Update messages_zh_CN.properties (#1599)
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2024-07-27 09:11:16 +01:00
Ludy
543b96c033 Add: Vietnam to the table (#1600)
* Add: Vietnam to the table

* Update labeler-config.yml
2024-07-27 08:37:22 +01:00
github-actions[bot]
c1126e57bd 📝 Update README: Translation Progress Table (#1598)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: GitHub Action action@github.com <GitHub Action action@github.com>
2024-07-26 16:01:23 +01:00
an-777
7c5077006d Update messages_zh_TW.properties: Translate English sentence to Traditional Chinese (#1596)
* Update messages_zh_TW.properties

* fix eof
2024-07-26 13:19:20 +01:00
github-actions[bot]
3e7889cee8 📝 Update README: Translation Progress Table (#1597)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: GitHub Action action@github.com <GitHub Action action@github.com>
2024-07-26 13:06:48 +01:00
Son Tran Lam
281047f42a Translate to Vietnamese (#1591)
* Translate to Vietnamese

* Add translation for Vietnamese

* - Remove  invalid properties and duplicated values.
- Add blank lines to align with the original format of en_GB file

* fix eof

* add empty lines to align with the template format. The number of line is equal with the reference en_GB file now

* change some translations to be more natural and easier to understand
2024-07-26 13:01:17 +01:00
github-actions[bot]
07f85ea8b4 📝 Update README: Translation Progress Table (#1587)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: GitHub Action action@github.com <GitHub Action action@github.com>
2024-07-26 13:00:54 +01:00
Jean-Baptiste WITTNER
e07f73dce7 [Helm][K8S] Add rootPath helm (#1593)
* Update of values.yaml to use a new variable to manage the application rootpath

* Use rootPath of values in deployment to manage rootPath and probes
2024-07-24 21:58:04 +01:00
albanobattistella
bfe38c71e8 Update messages_it_IT.properties (#1588) 2024-07-23 20:18:00 +01:00
Clara Bujeda
072090d41b Update of what was missing in messages_es_ES.properties (#1586)
I have translated what was missing from translating so everything would be translated.
2024-07-23 18:51:46 +01:00
Ludy
560936e182 remove new lines and obsolete spaces (#1585) 2024-07-23 16:57:21 +01:00
Ludy
6eb79e65fa minor changes in the DEV tools and more (#1578) 2024-07-22 21:15:10 +01:00
github-actions[bot]
cbe92269f4 Update 3rd Party Licenses (#1572)
Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: GitHub Action <action@github.com>
2024-07-20 10:49:09 +01:00
dependabot[bot]
81871a6f10 Bump springBootVersion from 3.3.0 to 3.3.2 (#1570)
Bumps `springBootVersion` from 3.3.0 to 3.3.2.

Updates `org.springframework.boot:spring-boot-starter-web` from 3.3.0 to 3.3.2
- [Release notes](https://github.com/spring-projects/spring-boot/releases)
- [Commits](https://github.com/spring-projects/spring-boot/compare/v3.3.0...v3.3.2)

Updates `org.springframework.boot:spring-boot-starter-jetty` from 3.3.0 to 3.3.2
- [Release notes](https://github.com/spring-projects/spring-boot/releases)
- [Commits](https://github.com/spring-projects/spring-boot/compare/v3.3.0...v3.3.2)

Updates `org.springframework.boot:spring-boot-starter-thymeleaf` from 3.3.0 to 3.3.2
- [Release notes](https://github.com/spring-projects/spring-boot/releases)
- [Commits](https://github.com/spring-projects/spring-boot/compare/v3.3.0...v3.3.2)

Updates `org.springframework.boot:spring-boot-starter-security` from 3.3.0 to 3.3.2
- [Release notes](https://github.com/spring-projects/spring-boot/releases)
- [Commits](https://github.com/spring-projects/spring-boot/compare/v3.3.0...v3.3.2)

Updates `org.springframework.boot:spring-boot-starter-data-jpa` from 3.3.0 to 3.3.2
- [Release notes](https://github.com/spring-projects/spring-boot/releases)
- [Commits](https://github.com/spring-projects/spring-boot/compare/v3.3.0...v3.3.2)

Updates `org.springframework.boot:spring-boot-starter-oauth2-client` from 3.3.0 to 3.3.2
- [Release notes](https://github.com/spring-projects/spring-boot/releases)
- [Commits](https://github.com/spring-projects/spring-boot/compare/v3.3.0...v3.3.2)

Updates `org.springframework.boot:spring-boot-starter-test` from 3.3.0 to 3.3.2
- [Release notes](https://github.com/spring-projects/spring-boot/releases)
- [Commits](https://github.com/spring-projects/spring-boot/compare/v3.3.0...v3.3.2)

Updates `org.springframework.boot:spring-boot-starter-actuator` from 3.3.0 to 3.3.2
- [Release notes](https://github.com/spring-projects/spring-boot/releases)
- [Commits](https://github.com/spring-projects/spring-boot/compare/v3.3.0...v3.3.2)

Updates `org.springframework.boot:spring-boot-devtools` from 3.3.0 to 3.3.2
- [Release notes](https://github.com/spring-projects/spring-boot/releases)
- [Commits](https://github.com/spring-projects/spring-boot/compare/v3.3.0...v3.3.2)

---
updated-dependencies:
- dependency-name: org.springframework.boot:spring-boot-starter-web
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.springframework.boot:spring-boot-starter-jetty
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.springframework.boot:spring-boot-starter-thymeleaf
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.springframework.boot:spring-boot-starter-security
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.springframework.boot:spring-boot-starter-data-jpa
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.springframework.boot:spring-boot-starter-oauth2-client
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.springframework.boot:spring-boot-starter-test
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.springframework.boot:spring-boot-starter-actuator
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.springframework.boot:spring-boot-devtools
  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-07-20 10:42:03 +01:00
github-actions[bot]
cf2a7896da 📝 Update README: Translation Progress Table (#1571)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: GitHub Action action@github.com <GitHub Action action@github.com>
2024-07-20 10:41:32 +01:00
Oğuz Ersen
6a3d95ba09 Add missing Turkish translation (#1549)
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2024-07-20 09:15:03 +00:00
Ludy
85ed0c38d1 Adding declaration as repository component & changing primary key type (#1559)
* Adding declaration as repository component & changing primary key type

* Update AuthorityRepository.java

* Update PersistentLoginRepository.java

---------

Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2024-07-20 10:13:49 +01:00
Ludy
6c7dc34640 Add: Label manager (#1560) 2024-07-20 09:57:27 +01:00
Anthony Stirling
ecfdfa5644 Update init-without-ocr.sh 2024-07-20 09:56:39 +01:00
Anthony Stirling
11e279bd12 Remove calibre for now 2024-07-20 09:54:46 +01:00
Anthony Stirling
929f0bbbe5 version bump, multi file fix and disable survey (#1550)
* version bump, multi file fix and disable survey

* example test stuff

* logs

* Update docker-compose-latest.yml

---------

Co-authored-by: a <a>
2024-07-20 09:53:58 +01:00
Ludy
5751b1ac2d adds Thai to the languages ​​table (#1555)
This PR makes #1554 obsolete
2024-07-11 23:35:01 +01:00
taesaeng28
4bf78ffd5d Added support for Thai language (#1551)
* Added support for Thai language

* Correct Thai national flag proportions in SVG

* Remove th_TH from env.LANGS

causer
Failed tests:
Stirling-PDF-Regression
Some tests failed.
Error: Process completed with exit code 1.

* fix incorrect syntax
2024-07-10 18:05:29 +00:00
pixeebot[bot]
b7d37deb85 Refactored to use parameterized SQL APIs (#1545)
Co-authored-by: pixeebot[bot] <104101892+pixeebot[bot]@users.noreply.github.com>
2024-07-09 21:18:32 +01:00
151 changed files with 10464 additions and 1489 deletions

View File

@@ -11,6 +11,20 @@ body:
This issue form is for reporting bugs only. Please fill out the following sections to help us understand the issue you are facing. This issue form is for reporting bugs only. Please fill out the following sections to help us understand the issue you are facing.
- type: dropdown
id: installation-method
attributes:
label: Installation Method
description: |
Indicate whether you are using Docker or a local installation.
options:
- Docker
- Docker ultra lite
- Docker fat
- Local Installation
validations:
required: true
- type: textarea - type: textarea
id: problem id: problem
validations: validations:

View File

@@ -1,6 +1,8 @@
name: Feature Request name: Feature Request
description: Submit a new feature request. description: Submit a new feature request.
title: "[Feature Request]: " title: "[Feature Request]: "
labels:
- enhancement
body: body:
- type: markdown - type: markdown
attributes: attributes:

View File

@@ -9,6 +9,8 @@ updates:
directory: "/" # Location of package manifests directory: "/" # Location of package manifests
schedule: schedule:
interval: "weekly" interval: "weekly"
open-pull-requests-limit: 10
rebase-strategy: "auto"
- package-ecosystem: "docker" - package-ecosystem: "docker"
directory: "/" # Location of Dockerfile directory: "/" # Location of Dockerfile
schedule: schedule:

View File

@@ -1,20 +1,54 @@
translation: Translation:
- changed-files: - changed-files:
- any-glob-to-any-file: 'src/main/resources/messages_*_*.properties' - any-glob-to-any-file: 'src/main/resources/messages_*_*.properties'
- any-glob-to-any-file: 'scripts/ignore_translation.toml'
- any-glob-to-any-file: 'src/main/resources/templates/fragments/languages.html'
Front End: Front End:
- changed-files: - changed-files:
- any-glob-to-any-file: 'src/main/resources/templates/**' - any-glob-to-any-file: 'src/main/resources/templates/**/*'
- any-glob-to-any-file: 'src/main/resources/static/**/*'
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/controller/web/**'
java: Java:
- changed-files: - changed-files:
- any-glob-to-any-file: 'src/main/java/**/*.java' - any-glob-to-any-file: 'src/main/java/**/*.java'
documentation: 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/resources/settings.yml.template'
- any-glob-to-any-file: 'src/main/resources/banner.txt'
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'
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/**/*'
Documentation:
- changed-files: - changed-files:
- any-glob-to-any-file: '**/*.md' - any-glob-to-any-file: '**/*.md'
- any-glob-to-any-file: 'scripts/counter_translation.py'
- any-glob-to-any-file: 'scripts/ignore_translation.toml'
docker: Docker:
- changed-files: - changed-files:
- any-glob-to-any-file: 'Dockerfile' - any-glob-to-any-file: 'Dockerfile'
- any-glob-to-any-file: 'Dockerfile-*' - any-glob-to-any-file: 'Dockerfile-*'
- any-glob-to-any-file: 'exampleYmlFiles/*.yml'
Test:
- changed-files:
- any-glob-to-any-file: 'cucumber/**/*'
- any-glob-to-any-file: 'src/test**/*'
Github:
- changed-files:
- any-glob-to-any-file: '.github/**/*'

93
.github/labels.yml vendored Normal file
View File

@@ -0,0 +1,93 @@
# Labels names are important as they are used by Release Drafter to decide
# regarding where to record them in changelog or if to skip them.
#
# The repository labels will be automatically configured using this file and
# the GitHub Action https://github.com/marketplace/actions/github-labeler.
- name: "Back End"
color: "20CE6C"
description: "Issues related to back-end development"
from_name: "Back end"
- name: "Bug"
description: "Something isn't working"
color: "EB9CA6"
from_name: "bug"
- name: "dependencies"
description: "Pull requests that update a dependency file"
color: "5AA8FC"
- name: "Docker"
description: "Pull requests that update Docker code"
color: "1FCEFF"
from_name: "docker"
- name: "Documentation"
description: "Improvements or additions to documentation"
color: "35ABFF"
from_name: "documentation"
- name: "Done for next release"
color: "0CDBD1"
- name: "Done"
color: "60F13B"
- name: "duplicate"
description: "This issue or pull request already exists"
color: "CDD1D5"
- name: "enhancement"
description: "New feature or request"
color: "A0EEEE"
- name: "fix needs confirmation"
color: "60A1E7"
description: "Fix needs to be confirmed"
- name: "Front End"
color: "BBD2F1"
description: "Issues related to front-end development"
- name: "github-actions"
description: "Pull requests that update GitHub Actions code"
color: "999999"
from_name: "github_actions"
- name: "good first issue"
description: "Good for newcomers"
color: "C1B8FF"
- name: "help wanted"
description: "Extra attention is needed"
color: "00E6C4"
- name: "invalid"
description: "This doesn't seem right"
color: "E5E566"
- name: "Java"
description: "Pull requests that update Java code"
color: "FF9E1F"
from_name: "java"
- name: "Long-term Enhancement"
color: "BFDEC3"
description: "Enhancements planned for the long term"
- name: "more-info-needed"
color: "00E4F8"
description: "More information is needed"
- name: "needs investigation"
color: "B8C3A7"
description: "Issues that require further investigation"
- name: "Prioritised enhancement"
color: "4BA2EE"
description: "High-priority enhancements"
- name: "question"
description: "Further information is requested"
color: "D97EE5"
- name: "Translation"
color: "9FABF9"
from_name: "translation"
- name: "upstream"
color: "DEDEDE"
- name: "v2"
color: "FFFF00"
- name: "wontfix"
description: "This will not be worked on"
color: "FFFFFF"
- name: "Security"
color: "000000"
description: "Security-related issues or pull requests"
- name: "API"
color: "FFFF00"
description: "API-related issues or pull requests"
- name: "Test"
color: "FF9E1F"
description: "Testing-related issues or pull requests"
- name: "Stale"
color: "000000"

View File

@@ -1,4 +1,5 @@
"""check_tabulator.py""" """check_tabulator.py"""
import argparse import argparse
import sys import sys

View File

@@ -11,7 +11,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/labeler@v5
- name: Apply Labels
uses: actions/labeler@v5
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
configuration-path: .github/labeler-config.yml configuration-path: .github/labeler-config.yml

View File

@@ -1,4 +1,4 @@
name: "Build repo" name: Build repo
on: on:
push: push:
@@ -17,20 +17,72 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix:
jdk-version: [17, 21]
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Set up JDK 17 - name: Set up JDK ${{ matrix.jdk-version }}
uses: actions/setup-java@v4 uses: actions/setup-java@v4
with: with:
java-version: "17" java-version: ${{ matrix.jdk-version }}
distribution: "temurin" distribution: "temurin"
- uses: gradle/actions/setup-gradle@v3 - name: Set up Gradle
uses: gradle/actions/setup-gradle@v4
with: with:
gradle-version: 8.7 gradle-version: 8.7
- name: Build with Gradle - name: Build with Gradle
run: ./gradlew build --no-build-cache run: ./gradlew build --no-build-cache
docker-compose-tests:
# if: github.event_name == 'push' && github.ref == 'refs/heads/main' ||
# (github.event_name == 'pull_request' &&
# contains(github.event.pull_request.labels.*.name, 'licenses') == false &&
# (
# contains(github.event.pull_request.labels.*.name, 'Front End') ||
# contains(github.event.pull_request.labels.*.name, 'Java') ||
# contains(github.event.pull_request.labels.*.name, 'Back End') ||
# contains(github.event.pull_request.labels.*.name, 'Security') ||
# contains(github.event.pull_request.labels.*.name, 'API') ||
# contains(github.event.pull_request.labels.*.name, 'Docker') ||
# contains(github.event.pull_request.labels.*.name, 'Test')
# )
# )
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Set up Java 17
uses: actions/setup-java@v4
with:
java-version: "17"
distribution: "adopt"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Install Docker Compose
run: |
sudo curl -SL "https://github.com/docker/compose/releases/download/v2.29.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.7"
- name: Pip requirements
run: |
pip install -r ./cucumber/requirements.txt
- name: Run Docker Compose Tests
run: |
chmod +x ./test.sh
./test.sh

View File

@@ -25,7 +25,7 @@ jobs:
java-version: "17" java-version: "17"
distribution: "adopt" distribution: "adopt"
- uses: gradle/actions/setup-gradle@v3 - uses: gradle/actions/setup-gradle@v4
- name: Run Gradle Command - name: Run Gradle Command
run: ./gradlew clean generateLicenseReport run: ./gradlew clean generateLicenseReport
@@ -45,6 +45,7 @@ jobs:
git diff --staged --quiet || echo "CHANGES_DETECTED=true" >> $GITHUB_ENV git diff --staged --quiet || echo "CHANGES_DETECTED=true" >> $GITHUB_ENV
- name: Create Pull Request - name: Create Pull Request
id: cpr
if: env.CHANGES_DETECTED == 'true' if: env.CHANGES_DETECTED == 'true'
uses: peter-evans/create-pull-request@v6 uses: peter-evans/create-pull-request@v6
with: with:
@@ -57,6 +58,22 @@ jobs:
title: "Update 3rd Party Licenses" title: "Update 3rd Party Licenses"
body: | body: |
Auto-generated by [create-pull-request][1] Auto-generated by [create-pull-request][1]
[1]: https://github.com/peter-evans/create-pull-request [1]: https://github.com/peter-evans/create-pull-request
labels: licenses
draft: false draft: false
delete-branch: true delete-branch: true
- name: Auto approve
if: steps.cpr.outputs.pull-request-operation == 'created'
run: gh pr review --approve "${{ steps.cpr.outputs.pull-request-number }}"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Enable auto-merge
if: steps.cpr.outputs.pull-request-operation == 'created'
uses: peter-evans/enable-pull-request-automerge@v3
with:
token: ${{ secrets.GITHUB_TOKEN }}
pull-request-number: ${{ steps.cpr.outputs.pull-request-number }}
merge-method: squash # Choose the merge method: merge, squash, or rebase

24
.github/workflows/manage-label.yml vendored Normal file
View File

@@ -0,0 +1,24 @@
name: Manage labels
on:
schedule:
- cron: "30 20 * * *"
permissions:
contents: read
issues: write
jobs:
labeler:
name: Labeler
runs-on: ubuntu-latest
steps:
- name: Check out the repository
uses: actions/checkout@v4
- name: Run Labeler
uses: crazy-max/ghaction-github-labeler@v5
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
yaml-file: .github/labels.yml
skip-delete: true

View File

@@ -22,7 +22,7 @@ jobs:
java-version: "17" java-version: "17"
distribution: "temurin" distribution: "temurin"
- uses: gradle/actions/setup-gradle@v3 - uses: gradle/actions/setup-gradle@v4
with: with:
gradle-version: 8.7 gradle-version: 8.7
@@ -72,7 +72,7 @@ jobs:
type=raw,value=alpha,enable=${{ github.ref == 'refs/heads/main' }} type=raw,value=alpha,enable=${{ github.ref == 'refs/heads/main' }}
- name: Build and push main Dockerfile - name: Build and push main Dockerfile
uses: docker/build-push-action@v5 uses: docker/build-push-action@v6
with: with:
builder: ${{ steps.buildx.outputs.name }} builder: ${{ steps.buildx.outputs.name }}
context: . context: .
@@ -98,7 +98,7 @@ jobs:
type=raw,value=latest-ultra-lite,enable=${{ github.ref == 'refs/heads/master' }} type=raw,value=latest-ultra-lite,enable=${{ github.ref == 'refs/heads/master' }}
- name: Build and push Dockerfile-ultra-lite - name: Build and push Dockerfile-ultra-lite
uses: docker/build-push-action@v5 uses: docker/build-push-action@v6
if: github.ref != 'refs/heads/main' if: github.ref != 'refs/heads/main'
with: with:
context: . context: .
@@ -111,7 +111,6 @@ jobs:
build-args: VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }} build-args: VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
platforms: linux/amd64,linux/arm64/v8 platforms: linux/amd64,linux/arm64/v8
- name: Generate tags fat - name: Generate tags fat
id: meta3 id: meta3
uses: docker/metadata-action@v5 uses: docker/metadata-action@v5
@@ -125,7 +124,7 @@ jobs:
type=raw,value=latest-fat,enable=${{ github.ref == 'refs/heads/master' }} type=raw,value=latest-fat,enable=${{ github.ref == 'refs/heads/master' }}
- name: Build and push main Dockerfile fat - name: Build and push main Dockerfile fat
uses: docker/build-push-action@v5 uses: docker/build-push-action@v6
if: github.ref != 'refs/heads/main' if: github.ref != 'refs/heads/main'
with: with:
builder: ${{ steps.buildx.outputs.name }} builder: ${{ steps.buildx.outputs.name }}

View File

@@ -27,7 +27,7 @@ jobs:
java-version: "17" java-version: "17"
distribution: "temurin" distribution: "temurin"
- uses: gradle/actions/setup-gradle@v3 - uses: gradle/actions/setup-gradle@v4
with: with:
gradle-version: 8.7 gradle-version: 8.7

View File

@@ -18,7 +18,7 @@ jobs:
java-version: "17" java-version: "17"
distribution: "temurin" distribution: "temurin"
- uses: gradle/actions/setup-gradle@v3 - uses: gradle/actions/setup-gradle@v4
- name: Generate Swagger documentation - name: Generate Swagger documentation
run: ./gradlew generateOpenApiDocs run: ./gradlew generateOpenApiDocs

View File

@@ -51,6 +51,7 @@ jobs:
[1]: https://github.com/peter-evans/create-pull-request [1]: https://github.com/peter-evans/create-pull-request
draft: false draft: false
delete-branch: true delete-branch: true
labels: github-actions
sync-readme: sync-readme:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@@ -88,3 +89,4 @@ jobs:
[1]: https://github.com/peter-evans/create-pull-request [1]: https://github.com/peter-evans/create-pull-request
draft: false draft: false
delete-branch: true delete-branch: true
labels: Documentation,Translation,github-actions

View File

@@ -1,47 +0,0 @@
name: Docker Compose Tests
on:
pull_request:
paths:
- "src/**"
- "**.gradle"
- "!src/main/java/resources/messages*"
- "exampleYmlFiles/**"
- "Dockerfile"
- "Dockerfile**"
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Set up Java 17
uses: actions/setup-java@v4
with:
java-version: "17"
distribution: "adopt"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Install Docker Compose
run: |
sudo curl -SL "https://github.com/docker/compose/releases/download/v2.26.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
# sudo chmod +x /usr/local/bin/docker-compose
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.7"
- name: Pip requirements
run: |
pip install -r ./cucumber/requirements.txt
- name: Run Docker Compose Tests
run: |
chmod +x ./test.sh
./test.sh

43
.gitignore vendored
View File

@@ -1,5 +1,3 @@
### Eclipse ### ### Eclipse ###
.metadata .metadata
bin/ bin/
@@ -22,7 +20,6 @@ customFiles/
configs/ configs/
watchedFolders/ watchedFolders/
# Gradle # Gradle
.gradle .gradle
.lock .lock
@@ -119,8 +116,28 @@ watchedFolders/
*.db *.db
/build /build
/.vscode # Byte-compiled / optimized / DLL files
/.idea __pycache__/
*.py[cod]
*.pyo
# Virtual environments
.env*
.venv*
env*/
venv*/
ENV/
env.bak/
venv.bak/
# VS Code
/.vscode/**/*
!/.vscode/settings.json
# IntelliJ IDEA
.idea/
*.iml
out/
# Ignore Mac DS_Store files # Ignore Mac DS_Store files
.DS_Store .DS_Store
@@ -128,3 +145,19 @@ watchedFolders/
# cucumber # cucumber
/cucumber/reports/** /cucumber/reports/**
# Certs
*.p12
*.pem
*.crt
*.cer
*.der
*.key
*.csr
# cache
.ruff_cache
.mypy_cache
.pytest_cache
.ipynb_checkpoints

53
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,53 @@
{
"java.compile.nullAnalysis.mode": "automatic",
"files.eol": "auto",
"java.configuration.updateBuildConfiguration": "interactive",
"black-formatter.args": ["--line-length", "127"],
"flake8.args": ["--max-line-length", "127"],
"pylint.args": ["max-line-length", "127"],
"[java]": {
"editor.tabSize": 4,
"editor.detectIndentation": false,
"editor.rulers": [127]
},
"[python]": {
"editor.tabSize": 2,
"editor.detectIndentation": false,
"editor.rulers": [127]
},
"[gradle-build]": {
"editor.tabSize": 4,
"editor.detectIndentation": false,
"editor.rulers": [127]
},
"[gradle]": {
"editor.tabSize": 4,
"editor.detectIndentation": false,
"editor.rulers": [127]
},
"[html]": {
"editor.tabSize": 2,
"editor.rulers": [127],
"files.trimFinalNewlines": false,
"files.insertFinalNewline": false
},
"[javascript]": {
"editor.tabSize": 2,
"editor.rulers": [127]
},
"[yaml]": {
"files.trimFinalNewlines": false,
"files.insertFinalNewline": false
},
"diffEditor.maxComputationTime": 0,
"editor.wordSegmenterLocales": null,
"editor.guides.bracketPairs": "active",
"editor.guides.bracketPairsHorizontal": "active",
"files.insertFinalNewline": true,
"files.trimFinalNewlines": true,
"files.trimTrailingWhitespace": true,
"editor.indentSize": "tabSize",
"editor.stickyScroll.enabled": false,
"editor.minimap.enabled": false,
"editor.formatOnSave": true
}

View File

@@ -1,5 +1,5 @@
# Main stage # Main stage
FROM alpine:3.20.0 FROM alpine:3.20.2
# Copy necessary files # Copy necessary files
COPY scripts /scripts COPY scripts /scripts
@@ -39,16 +39,16 @@ RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /et
libreoffice \ libreoffice \
# pdftohtml # pdftohtml
poppler-utils \ poppler-utils \
# OCR MY PDF (unpaper for descew and other advanced featues) # OCR MY PDF (unpaper for descew and other advanced features)
ocrmypdf \ ocrmypdf \
tesseract-ocr-data-eng \ tesseract-ocr-data-eng \
# CV # CV
py3-opencv \ py3-opencv \
# python3/pip # python3/pip
python3 && \ python3 \
wget https://bootstrap.pypa.io/get-pip.py -qO - | python3 - --break-system-packages --no-cache-dir --upgrade && \ py3-pip && \
# uno unoconv and HTML # uno unoconv and HTML
pip install --break-system-packages --no-cache-dir --upgrade unoconv WeasyPrint && \ pip install --break-system-packages --no-cache-dir --upgrade unoconv WeasyPrint pdf2image pillow && \
mv /usr/share/tessdata /usr/share/tessdata-original && \ mv /usr/share/tessdata /usr/share/tessdata-original && \
mkdir -p $HOME /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders && \ mkdir -p $HOME /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders && \
fc-cache -f -v && \ fc-cache -f -v && \

View File

@@ -12,7 +12,7 @@ RUN DOCKER_ENABLE_SECURITY=true \
./gradlew clean build ./gradlew clean build
# Main stage # Main stage
FROM alpine:3.20.0 FROM alpine:3.20.2
# Copy necessary files # Copy necessary files
COPY scripts /scripts COPY scripts /scripts
@@ -31,7 +31,7 @@ ENV DOCKER_ENABLE_SECURITY=false \
PGID=1000 \ PGID=1000 \
UMASK=022 \ UMASK=022 \
FAT_DOCKER=true \ FAT_DOCKER=true \
INSTALL_BOOK_AND_ADVANCED_HTML_OPS=true INSTALL_BOOK_AND_ADVANCED_HTML_OPS=false
# JDK for app # JDK for app
@@ -45,7 +45,6 @@ RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /et
tini \ tini \
bash \ bash \
curl \ curl \
calibre@testing \
shadow \ shadow \
su-exec \ su-exec \
openssl \ openssl \
@@ -62,10 +61,10 @@ RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /et
# CV # CV
py3-opencv \ py3-opencv \
# python3/pip # python3/pip
python3 && \ python3 \
wget https://bootstrap.pypa.io/get-pip.py -qO - | python3 - --break-system-packages --no-cache-dir --upgrade && \ py3-pip && \
# uno unoconv and HTML # uno unoconv and HTML
pip install --break-system-packages --no-cache-dir --upgrade unoconv WeasyPrint && \ pip install --break-system-packages --no-cache-dir --upgrade unoconv WeasyPrint pdf2image pillow && \
mv /usr/share/tessdata /usr/share/tessdata-original && \ mv /usr/share/tessdata /usr/share/tessdata-original && \
mkdir -p $HOME /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders && \ mkdir -p $HOME /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders && \
fc-cache -f -v && \ fc-cache -f -v && \

View File

@@ -1,5 +1,5 @@
# use alpine # use alpine
FROM alpine:3.20.0 FROM alpine:3.20.2
ARG VERSION_TAG ARG VERSION_TAG

View File

@@ -15,7 +15,7 @@
| file-to-pdf | | ✔️ | | | ✔️ | | | ✔️ | | | | | file-to-pdf | | ✔️ | | | ✔️ | | | ✔️ | | | |
| img-to-pdf | | ✔️ | | | | | | | | ✔️ | | | img-to-pdf | | ✔️ | | | | | | | | ✔️ | |
| pdf-to-html | | ✔️ | | | ✔️ | | | ✔️ | | | | | pdf-to-html | | ✔️ | | | ✔️ | | | ✔️ | | | |
| pdf-to-img | | ✔️ | | | | | | | | ✔️ | | | pdf-to-img | | ✔️ | | | | ✔️ | | | | ✔️ | |
| pdf-to-pdfa | | ✔️ | | | ✔️ | | | | ✔️ | | | | pdf-to-pdfa | | ✔️ | | | ✔️ | | | | ✔️ | | |
| pdf-to-markdown | | ✔️ | | | | | | | | ✔️ | | | pdf-to-markdown | | ✔️ | | | | | | | | ✔️ | |
| pdf-to-presentation | | ✔️ | | | ✔️ | | | ✔️ | | | | | pdf-to-presentation | | ✔️ | | | ✔️ | | | ✔️ | | | |

View File

@@ -165,42 +165,46 @@ Please view https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToUseOCR
## Supported Languages ## Supported Languages
Stirling PDF currently supports 32! Stirling PDF currently supports 38!
| Language | Progress | | Language | Progress |
| ------------------------------------------- | -------------------------------------- | | ------------------------------------------- | -------------------------------------- |
| Arabic (العربية) (ar_AR) | ![44%](https://geps.dev/progress/44) |
| Basque (Euskara) (eu_ES) | ![60%](https://geps.dev/progress/60) |
| Bulgarian (Български) (bg_BG) | ![92%](https://geps.dev/progress/92) |
| Catalan (Català) (ca_CA) | ![47%](https://geps.dev/progress/47) |
| Croatian (Hrvatski) (hr_HR) | ![93%](https://geps.dev/progress/93) |
| Czech (Česky) (cs_CZ) | ![88%](https://geps.dev/progress/88) |
| Danish (Dansk) (da_DK) | ![9%](https://geps.dev/progress/9) |
| Dutch (Nederlands) (nl_NL) | ![94%](https://geps.dev/progress/94) |
| English (English) (en_GB) | ![100%](https://geps.dev/progress/100) | | English (English) (en_GB) | ![100%](https://geps.dev/progress/100) |
| English (US) (en_US) | ![100%](https://geps.dev/progress/100) | | English (US) (en_US) | ![100%](https://geps.dev/progress/100) |
| Arabic (العربية) (ar_AR) | ![45%](https://geps.dev/progress/45) | | French (Français) (fr_FR) | ![91%](https://geps.dev/progress/91) |
| German (Deutsch) (de_DE) | ![100%](https://geps.dev/progress/100) | | German (Deutsch) (de_DE) | ![98%](https://geps.dev/progress/98) |
| French (Français) (fr_FR) | ![94%](https://geps.dev/progress/94) | | Greek (Ελληνικά) (el_GR) | ![80%](https://geps.dev/progress/80) |
| Spanish (Español) (es_ES) | ![92%](https://geps.dev/progress/92) | | Hindi (हिंदी) (hi_IN) | ![75%](https://geps.dev/progress/75) |
| Simplified Chinese (简体中文) (zh_CN) | ![98%](https://geps.dev/progress/98) | | Hungarian (Magyar) (hu_HU) | ![74%](https://geps.dev/progress/74) |
| Traditional Chinese (繁體中文) (zh_TW) | ![96%](https://geps.dev/progress/96) | | Indonesia (Bahasa Indonesia) (id_ID) | ![75%](https://geps.dev/progress/75) |
| Catalan (Català) (ca_CA) | ![48%](https://geps.dev/progress/48) | | Irish (Gaeilge) (ga_IE) | ![96%](https://geps.dev/progress/96) |
| Italian (Italiano) (it_IT) | ![99%](https://geps.dev/progress/99) | | Italian (Italiano) (it_IT) | ![98%](https://geps.dev/progress/98) |
| Swedish (Svenska) (sv_SE) | ![39%](https://geps.dev/progress/39) | | Japanese (日本語) (ja_JP) | ![91%](https://geps.dev/progress/91) |
| Polish (Polski) (pl_PL) | ![90%](https://geps.dev/progress/90) | | Korean (한국어) (ko_KR) | ![82%](https://geps.dev/progress/82) |
| Romanian (Română) (ro_RO) | ![39%](https://geps.dev/progress/39) |
| Korean (한국어) (ko_KR) | ![84%](https://geps.dev/progress/84) |
| Portuguese Brazilian (Português) (pt_BR) | ![60%](https://geps.dev/progress/60) |
| Portuguese (Português) (pt_PT) | ![78%](https://geps.dev/progress/78) |
| Russian (Русский) (ru_RU) | ![84%](https://geps.dev/progress/84) |
| Basque (Euskara) (eu_ES) | ![62%](https://geps.dev/progress/62) |
| Japanese (日本語) (ja_JP) | ![90%](https://geps.dev/progress/90) |
| Dutch (Nederlands) (nl_NL) | ![96%](https://geps.dev/progress/96) |
| Greek (Ελληνικά) (el_GR) | ![82%](https://geps.dev/progress/82) |
| Turkish (Türkçe) (tr_TR) | ![94%](https://geps.dev/progress/94) |
| Indonesia (Bahasa Indonesia) (id_ID) | ![76%](https://geps.dev/progress/76) |
| Hindi (हिंदी) (hi_IN) | ![77%](https://geps.dev/progress/77) |
| Hungarian (Magyar) (hu_HU) | ![76%](https://geps.dev/progress/76) |
| Bulgarian (Български) (bg_BG) | ![94%](https://geps.dev/progress/94) |
| Sebian Latin alphabet (Srpski) (sr_LATN_RS) | ![78%](https://geps.dev/progress/78) |
| Ukrainian (Українська) (uk_UA) | ![90%](https://geps.dev/progress/90) |
| Slovakian (Slovensky) (sk_SK) | ![92%](https://geps.dev/progress/92) |
| Czech (Česky) (cs_CZ) | ![90%](https://geps.dev/progress/90) |
| Croatian (Hrvatski) (hr_HR) | ![95%](https://geps.dev/progress/95) |
| Norwegian (Norsk) (no_NB) | ![96%](https://geps.dev/progress/96) | | Norwegian (Norsk) (no_NB) | ![96%](https://geps.dev/progress/96) |
| Polish (Polski) (pl_PL) | ![90%](https://geps.dev/progress/90) |
| Portuguese (Português) (pt_PT) | ![77%](https://geps.dev/progress/77) |
| Portuguese Brazilian (Português) (pt_BR) | ![99%](https://geps.dev/progress/99) |
| Romanian (Română) (ro_RO) | ![38%](https://geps.dev/progress/38) |
| Russian (Русский) (ru_RU) | ![82%](https://geps.dev/progress/82) |
| Serbian Latin alphabet (Srpski) (sr_LATN_RS) | ![77%](https://geps.dev/progress/77) |
| Simplified Chinese (简体中文) (zh_CN) | ![97%](https://geps.dev/progress/97) |
| Slovakian (Slovensky) (sk_SK) | ![90%](https://geps.dev/progress/90) |
| Spanish (Español) (es_ES) | ![96%](https://geps.dev/progress/96) |
| Swedish (Svenska) (sv_SE) | ![38%](https://geps.dev/progress/38) |
| Thai (ไทย) (th_TH) | ![97%](https://geps.dev/progress/97) |
| Traditional Chinese (繁體中文) (zh_TW) | ![96%](https://geps.dev/progress/96) |
| Turkish (Türkçe) (tr_TR) | ![97%](https://geps.dev/progress/97) |
| Ukrainian (Українська) (uk_UA) | ![88%](https://geps.dev/progress/88) |
| Vietnamese (Tiếng Việt) (vi_VN) | ![97%](https://geps.dev/progress/97) |
## Contributing (creating issues, translations, fixing bugs, etc.) ## Contributing (creating issues, translations, fixing bugs, etc.)
@@ -262,6 +266,7 @@ security:
clientId: '' # Client ID from your provider clientId: '' # Client ID from your provider
clientSecret: '' # Client Secret from your provider clientSecret: '' # Client Secret from your provider
autoCreateUser: false # set to 'true' to allow auto-creation of non-existing users 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 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 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' provider: google # Set this to your OAuth provider's name, e.g., 'google' or 'keycloak'

View File

@@ -1,25 +1,32 @@
plugins { plugins {
id "java" id "java"
id "org.springframework.boot" version "3.3.0" id "org.springframework.boot" version "3.3.2"
id "io.spring.dependency-management" version "1.1.5" id "io.spring.dependency-management" version "1.1.6"
id "org.springdoc.openapi-gradle-plugin" version "1.8.0" id "org.springdoc.openapi-gradle-plugin" version "1.8.0"
id "io.swagger.swaggerhub" version "1.3.2" id "io.swagger.swaggerhub" version "1.3.2"
id "edu.sc.seis.launch4j" version "3.0.5" id "edu.sc.seis.launch4j" version "3.0.6"
id "com.diffplug.spotless" version "6.25.0" id "com.diffplug.spotless" version "6.25.0"
id "com.github.jk1.dependency-license-report" version "2.8" id "com.github.jk1.dependency-license-report" version "2.9"
} }
import com.github.jk1.license.render.* import com.github.jk1.license.render.*
ext { ext {
springBootVersion = "3.3.0" springBootVersion = "3.3.2"
pdfboxVersion = "3.0.3"
logbackVersion = "1.5.7"
imageioVersion = "3.11.0"
lombokVersion = "1.18.34"
bouncycastleVersion = "1.78.1"
} }
group = "stirling.software" group = "stirling.software"
version = "0.26.1" version = "0.28.2"
java {
// 17 is lowest but we support and recommend 21 // 17 is lowest but we support and recommend 21
sourceCompatibility = "17" sourceCompatibility = JavaVersion.VERSION_17
}
repositories { repositories {
mavenCentral() mavenCentral()
@@ -40,8 +47,10 @@ sourceSets {
exclude "stirling/software/SPDF/controller/web/AccountWebController.java" exclude "stirling/software/SPDF/controller/web/AccountWebController.java"
exclude "stirling/software/SPDF/controller/web/DatabaseWebController.java" exclude "stirling/software/SPDF/controller/web/DatabaseWebController.java"
exclude "stirling/software/SPDF/model/ApiKeyAuthenticationToken.java" exclude "stirling/software/SPDF/model/ApiKeyAuthenticationToken.java"
exclude "stirling/software/SPDF/model/AttemptCounter.java"
exclude "stirling/software/SPDF/model/Authority.java" exclude "stirling/software/SPDF/model/Authority.java"
exclude "stirling/software/SPDF/model/PersistentLogin.java" exclude "stirling/software/SPDF/model/PersistentLogin.java"
exclude "stirling/software/SPDF/model/SessionEntity.java"
exclude "stirling/software/SPDF/model/User.java" exclude "stirling/software/SPDF/model/User.java"
exclude "stirling/software/SPDF/repository/**" exclude "stirling/software/SPDF/repository/**"
} }
@@ -97,11 +106,11 @@ tasks.wrapper {
dependencies { dependencies {
//security updates //security updates
implementation "ch.qos.logback:logback-classic:1.5.6" implementation "ch.qos.logback:logback-classic:$logbackVersion"
implementation "ch.qos.logback:logback-core:1.5.6" implementation "ch.qos.logback:logback-core:$logbackVersion"
implementation "org.springframework:spring-webmvc:6.1.9" implementation "org.springframework:spring-webmvc:6.1.9"
implementation("io.github.pixee:java-security-toolkit:1.1.3") implementation("io.github.pixee:java-security-toolkit:1.2.0")
// implementation "org.yaml:snakeyaml:2.2" // implementation "org.yaml:snakeyaml:2.2"
implementation 'com.github.Carleslc.Simple-YAML:Simple-Yaml:1.8.4' implementation 'com.github.Carleslc.Simple-YAML:Simple-Yaml:1.8.4'
@@ -131,22 +140,22 @@ dependencies {
implementation "org.apache.xmlgraphics:batik-all:1.17" implementation "org.apache.xmlgraphics:batik-all:1.17"
// TwelveMonkeys // TwelveMonkeys
implementation "com.twelvemonkeys.imageio:imageio-batik:3.10.1" implementation "com.twelvemonkeys.imageio:imageio-batik:$imageioVersion"
implementation "com.twelvemonkeys.imageio:imageio-bmp:3.10.1" implementation "com.twelvemonkeys.imageio:imageio-bmp:$imageioVersion"
// implementation "com.twelvemonkeys.imageio:imageio-hdr:3.10.1" // implementation "com.twelvemonkeys.imageio:imageio-hdr:$imageioVersion"
// implementation "com.twelvemonkeys.imageio:imageio-icns:3.10.1" // implementation "com.twelvemonkeys.imageio:imageio-icns:$imageioVersion"
// implementation "com.twelvemonkeys.imageio:imageio-iff:3.10.1" // implementation "com.twelvemonkeys.imageio:imageio-iff:$imageioVersion"
implementation "com.twelvemonkeys.imageio:imageio-jpeg:3.11.0" implementation "com.twelvemonkeys.imageio:imageio-jpeg:$imageioVersion"
// implementation "com.twelvemonkeys.imageio:imageio-pcx:3.10.1" // implementation "com.twelvemonkeys.imageio:imageio-pcx:$imageioVersion@
// implementation "com.twelvemonkeys.imageio:imageio-pict:3.10.1" // implementation "com.twelvemonkeys.imageio:imageio-pict:$imageioVersion"
// implementation "com.twelvemonkeys.imageio:imageio-pnm:3.10.1" // implementation "com.twelvemonkeys.imageio:imageio-pnm:$imageioVersion"
// implementation "com.twelvemonkeys.imageio:imageio-psd:3.10.1" // implementation "com.twelvemonkeys.imageio:imageio-psd:$imageioVersion"
// implementation "com.twelvemonkeys.imageio:imageio-sgi:3.10.1" // implementation "com.twelvemonkeys.imageio:imageio-sgi:$imageioVersion"
// implementation "com.twelvemonkeys.imageio:imageio-tga:3.10.1" // implementation "com.twelvemonkeys.imageio:imageio-tga:$imageioVersion"
// implementation "com.twelvemonkeys.imageio:imageio-thumbsdb:3.10.1" // implementation "com.twelvemonkeys.imageio:imageio-thumbsdb:$imageioVersion"
implementation "com.twelvemonkeys.imageio:imageio-tiff:3.10.1" implementation "com.twelvemonkeys.imageio:imageio-tiff:$imageioVersion"
implementation "com.twelvemonkeys.imageio:imageio-webp:3.10.1" implementation "com.twelvemonkeys.imageio:imageio-webp:$imageioVersion"
// implementation "com.twelvemonkeys.imageio:imageio-xwd:3.10.1" // implementation "com.twelvemonkeys.imageio:imageio-xwd:$imageioVersion"
implementation "commons-io:commons-io:2.16.1" implementation "commons-io:commons-io:2.16.1"
implementation "org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0" implementation "org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0"
@@ -158,33 +167,36 @@ dependencies {
exclude group: "commons-logging", module: "commons-logging" exclude group: "commons-logging", module: "commons-logging"
} }
implementation ("org.apache.pdfbox:pdfbox:3.0.2") { implementation ("org.apache.pdfbox:pdfbox:$pdfboxVersion") {
exclude group: "commons-logging", module: "commons-logging" exclude group: "commons-logging", module: "commons-logging"
} }
implementation ("org.apache.pdfbox:xmpbox:3.0.2") { implementation ("org.apache.pdfbox:xmpbox:$pdfboxVersion") {
exclude group: "commons-logging", module: "commons-logging" exclude group: "commons-logging", module: "commons-logging"
} }
implementation 'org.apache.pdfbox:jbig2-imageio:3.0.4'
implementation "com.github.Carleslc.Simple-YAML:Simple-Yaml:1.8.4" implementation "com.github.Carleslc.Simple-YAML:Simple-Yaml:1.8.4"
implementation "org.bouncycastle:bcprov-jdk18on:1.78.1" implementation "org.bouncycastle:bcprov-jdk18on:$bouncycastleVersion"
implementation "org.bouncycastle:bcpkix-jdk18on:1.78.1" implementation "org.bouncycastle:bcpkix-jdk18on:$bouncycastleVersion"
implementation "org.springframework.boot:spring-boot-starter-actuator:$springBootVersion" implementation "org.springframework.boot:spring-boot-starter-actuator:$springBootVersion"
implementation "io.micrometer:micrometer-core:1.13.0" implementation "io.micrometer:micrometer-core:1.13.3"
implementation group: "com.google.zxing", name: "core", version: "3.5.3" implementation group: "com.google.zxing", name: "core", version: "3.5.3"
// https://mvnrepository.com/artifact/org.commonmark/commonmark // https://mvnrepository.com/artifact/org.commonmark/commonmark
implementation "org.commonmark:commonmark:0.22.0" implementation "org.commonmark:commonmark:0.22.0"
implementation "org.commonmark:commonmark-ext-gfm-tables:0.22.0" implementation "org.commonmark:commonmark-ext-gfm-tables:0.22.0"
// https://mvnrepository.com/artifact/com.bucket4j/bucket4j_jdk17 // https://mvnrepository.com/artifact/com.bucket4j/bucket4j_jdk17
implementation "com.bucket4j:bucket4j_jdk17-core:8.12.1" implementation "com.bucket4j:bucket4j_jdk17-core:8.14.0"
implementation "com.fathzer:javaluator:3.0.4" implementation "com.fathzer:javaluator:3.0.4"
developmentOnly("org.springframework.boot:spring-boot-devtools:$springBootVersion") developmentOnly("org.springframework.boot:spring-boot-devtools:$springBootVersion")
compileOnly "org.projectlombok:lombok:1.18.32" compileOnly "org.projectlombok:lombok:$lombokVersion"
annotationProcessor "org.projectlombok:lombok:1.18.32" annotationProcessor "org.projectlombok:lombok:$lombokVersion"
testImplementation 'org.mockito:mockito-inline:3.12.4' testImplementation 'org.mockito:mockito-inline:5.2.0'
} }
tasks.withType(JavaCompile).configureEach { tasks.withType(JavaCompile).configureEach {

View File

@@ -1,5 +1,5 @@
apiVersion: v2 apiVersion: v2
appVersion: 0.26.1 appVersion: 0.28.2
description: locally hosted web application that allows you to perform various operations description: locally hosted web application that allows you to perform various operations
on PDF files on PDF files
home: https://github.com/Stirling-Tools/Stirling-PDF home: https://github.com/Stirling-Tools/Stirling-PDF

View File

@@ -62,8 +62,10 @@ spec:
imagePullPolicy: {{ .Values.image.pullPolicy }} imagePullPolicy: {{ .Values.image.pullPolicy }}
securityContext: securityContext:
{{- toYaml .Values.containerSecurityContext | nindent 10 }} {{- toYaml .Values.containerSecurityContext | nindent 10 }}
{{- if .Values.envs }}
env: env:
- name: SYSTEM_ROOTURIPATH
value: {{ .Values.rootPath}}
{{- if .Values.envs }}
{{ toYaml .Values.envs | indent 8 }} {{ toYaml .Values.envs | indent 8 }}
{{- end }} {{- end }}
{{- if .Values.extraArgs }} {{- if .Values.extraArgs }}
@@ -75,13 +77,13 @@ spec:
containerPort: 8080 containerPort: 8080
livenessProbe: livenessProbe:
httpGet: httpGet:
path: / path: {{ .Values.rootPath}}
port: http port: http
{{ toYaml .Values.probes.livenessHttpGetConfig | indent 12 }} {{ toYaml .Values.probes.livenessHttpGetConfig | indent 12 }}
{{ toYaml .Values.probes.liveness | indent 10 }} {{ toYaml .Values.probes.liveness | indent 10 }}
readinessProbe: readinessProbe:
httpGet: httpGet:
path: / path: {{ .Values.rootPath}}
port: http port: http
{{ toYaml .Values.probes.readinessHttpGetConfig | indent 12 }} {{ toYaml .Values.probes.readinessHttpGetConfig | indent 12 }}
{{ toYaml .Values.probes.readiness | indent 10 }} {{ toYaml .Values.probes.readiness | indent 10 }}

View File

@@ -15,6 +15,9 @@ secret:
commonLabels: {} commonLabels: {}
# team_name: dev # team_name: dev
# rootpath for the application
rootPath: /
envs: [] envs: []
# - name: UI_APP_NAME # - name: UI_APP_NAME
# value: "Stirling PDF" # value: "Stirling PDF"
@@ -24,8 +27,6 @@ envs: []
# value: "Stirling PDF" # value: "Stirling PDF"
# - name: ALLOW_GOOGLE_VISIBILITY # - name: ALLOW_GOOGLE_VISIBILITY
# value: "true" # value: "true"
# - name: APP_ROOT_PATH
# value: "/"
# - name: APP_LOCALE # - name: APP_LOCALE
# value: "en_GB" # value: "en_GB"

View File

@@ -0,0 +1,106 @@
%PDF-1.3
%“Œ‹ž ReportLab Generated PDF document http://www.reportlab.com
1 0 obj
<<
/F1 2 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/Contents 9 0 R /MediaBox [ 0 0 612 792 ] /Parent 8 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
4 0 obj
<<
/Contents 10 0 R /MediaBox [ 0 0 612 792 ] /Parent 8 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
5 0 obj
<<
/Contents 11 0 R /MediaBox [ 0 0 612 792 ] /Parent 8 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
6 0 obj
<<
/PageMode /UseNone /Pages 8 0 R /Type /Catalog
>>
endobj
7 0 obj
<<
/Author (anonymous) /CreationDate (D:20240718233034+00'00') /Creator (ReportLab PDF Library - www.reportlab.com) /Keywords () /ModDate (D:20240718233034+00'00') /Producer (ReportLab PDF Library - www.reportlab.com)
/Subject (unspecified) /Title (untitled) /Trapped /False
>>
endobj
8 0 obj
<<
/Count 3 /Kids [ 3 0 R 4 0 R 5 0 R ] /Type /Pages
>>
endobj
9 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 210
>>
stream
Gap@Gb79+X'F"5[`EfJOD4:mD<%*=m+N>oDG,>NK`<U'B^0WYY,dWl^i_UcRk`<"L=<NPC$BtQ<5l$3<Y!?BuoCSYQ6GSt25lpqr0IrP?S[b)9%M"e'HHFqcRO'9eRaR0'DYi*Y.:nEMFAoTM;rPL%EF]`CfoELVl_Q,"LS:%iI;Nc[&bG.*65O]ecfK1'*<>5P_s[usI/ph*0pV~>endstream
endobj
10 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 209
>>
stream
Gap@Gb79+X'F"5Y`EfJOV2A9=!fB]F'tK1LS`,]G+MiTenb&V2-^hqa(5IE#Nr59/!"Qm*5_(BdF!0&h!Yhk/A+\iS'%6tuO$O)9LaZS+flr([1p2&#RS1p/gT[B;rDj-=&=iqUlj(P^/5U@eCFqn4:<lU`l`.HXqG-',hJH.DI.(6L\luSAW`Q'oje[qgVLVIXg%PXe+,<$7('~>endstream
endobj
11 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 209
>>
stream
Gap@GbmK%f(e+0_`ODoa2.):e/i+N3r(.o*Qf\gSNb(bt4FIubi@GIOE=p8Ir3;CbQ@KuG^cdJhODZKQ*upt+*rdZ%!mFmN$*.P)K;`s#]G=8AO3s3DGB.RCOn?[F]bEIg,a>25?B%dh\Z/C6opFE'el@I,P\u\V\]:*JYrrsNJ&d,11VL;$h!43eGu&1X6$+5-h\Vr6!+>4Je,~>endstream
endobj
xref
0 12
0000000000 65535 f
0000000073 00000 n
0000000104 00000 n
0000000211 00000 n
0000000404 00000 n
0000000598 00000 n
0000000792 00000 n
0000000860 00000 n
0000001156 00000 n
0000001227 00000 n
0000001527 00000 n
0000001827 00000 n
trailer
<<
/ID
[<0d5cf047e754e05f8d574f067785875c><0d5cf047e754e05f8d574f067785875c>]
% ReportLab generated PDF document -- digest (http://www.reportlab.com)
/Info 7 0 R
/Root 6 0 R
/Size 12
>>
startxref
2127
%%EOF

View File

@@ -0,0 +1,106 @@
%PDF-1.3
%“Œ‹ž ReportLab Generated PDF document http://www.reportlab.com
1 0 obj
<<
/F1 2 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/Contents 9 0 R /MediaBox [ 0 0 612 792 ] /Parent 8 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
4 0 obj
<<
/Contents 10 0 R /MediaBox [ 0 0 612 792 ] /Parent 8 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
5 0 obj
<<
/Contents 11 0 R /MediaBox [ 0 0 612 792 ] /Parent 8 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
6 0 obj
<<
/PageMode /UseNone /Pages 8 0 R /Type /Catalog
>>
endobj
7 0 obj
<<
/Author (anonymous) /CreationDate (D:20240718233034+00'00') /Creator (ReportLab PDF Library - www.reportlab.com) /Keywords () /ModDate (D:20240718233034+00'00') /Producer (ReportLab PDF Library - www.reportlab.com)
/Subject (unspecified) /Title (untitled) /Trapped /False
>>
endobj
8 0 obj
<<
/Count 3 /Kids [ 3 0 R 4 0 R 5 0 R ] /Type /Pages
>>
endobj
9 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 207
>>
stream
Gap@G:CDb.*/<p2MVk["e@)7*Z0@"b%+@f/9pA%_U<oOkVp?PnGRb81iPg?0i?(]%^_CSf##%;<!7Ne/-%RR^p@t7hKYZ9eJVHV]fjjHIB:6DrW+2\p16@*`r^CpQZZH'2Pjqd<.&hM2UO%$Wi$te%4QmS;<E"QS\!deQG_XtuEK>b(UbS>%`/0S`k\\5'TNY0mmgH?`8]i_0~>endstream
endobj
10 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 207
>>
stream
Gap@G]afWJ'Lm;=if<;s>V*7BTJ]oQ@P!(q5S+WG1%>L@?8Ue;c>[fY&&IOd5@t@TY@+q.5T<Z'81"J("KhsBa+&u4"n'#6)AjfImh)%$0tVC:aGk",=aJJH#/4]i.WJr9c"cibYm:M-44<%FFlG0Cl\Z'nmo7C"TR+7dk3T#iD(9Pq'\;rQku%o>A_`50SO&7M04=8M'O<Am~>endstream
endobj
11 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 209
>>
stream
Gap@GYmu@>'Ld5[if35r/JNaJ.A.7fP9RpSN*8k^-sEER0,enq1Rsuo@R/uCO-^&Y`F'9d^a?9)?ns+F&dXm[HMgPn6Ep+%TRk5Nh+!(+[H#H:U^.^(YL,PKS'%j/:3O\hJVEK-UUekJTd[A$N^((K^#0Du`i@,/^f5KiUISGr")3/+f9NF8NO1+iUgm^b"X\cE^+[:s!0]Gu6i~>endstream
endobj
xref
0 12
0000000000 65535 f
0000000073 00000 n
0000000104 00000 n
0000000211 00000 n
0000000404 00000 n
0000000598 00000 n
0000000792 00000 n
0000000860 00000 n
0000001156 00000 n
0000001227 00000 n
0000001524 00000 n
0000001822 00000 n
trailer
<<
/ID
[<407fc55425168745e56176202aad30c9><407fc55425168745e56176202aad30c9>]
% ReportLab generated PDF document -- digest (http://www.reportlab.com)
/Info 7 0 R
/Root 6 0 R
/Size 12
>>
startxref
2122
%%EOF

View File

@@ -0,0 +1,106 @@
%PDF-1.3
%“Œ‹ž ReportLab Generated PDF document http://www.reportlab.com
1 0 obj
<<
/F1 2 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/Contents 9 0 R /MediaBox [ 0 0 612 792 ] /Parent 8 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
4 0 obj
<<
/Contents 10 0 R /MediaBox [ 0 0 612 792 ] /Parent 8 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
5 0 obj
<<
/Contents 11 0 R /MediaBox [ 0 0 612 792 ] /Parent 8 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
6 0 obj
<<
/PageMode /UseNone /Pages 8 0 R /Type /Catalog
>>
endobj
7 0 obj
<<
/Author (anonymous) /CreationDate (D:20240718233034+00'00') /Creator (ReportLab PDF Library - www.reportlab.com) /Keywords () /ModDate (D:20240718233034+00'00') /Producer (ReportLab PDF Library - www.reportlab.com)
/Subject (unspecified) /Title (untitled) /Trapped /False
>>
endobj
8 0 obj
<<
/Count 3 /Kids [ 3 0 R 4 0 R 5 0 R ] /Type /Pages
>>
endobj
9 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 209
>>
stream
Gap@G]+0EH(e/_@iZH]:>:>hu1e>07BJg5<'#:.C1n)e#(QJ6R1Rsuo_gpn.+0-H5$/#"iYR[B.9\'>7!aDAC*rf/t&6O#aH<?-7IT'\?X(&TcABG=ON*Nq`4k=o&p@3,0*31r<)TAP2Pk94p0\"R-_sY1$AYo[8B\?4R>feLAB\mpjZhp"`@J3;"Fm97#9+W,"eb95\+#p\^HN~>endstream
endobj
10 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 209
>>
stream
Gap@G]+0EX'Eriuig+>QHNeD'#n%Sq#n%BW`C'uDUOYK)HdS4E9JMsp+HUmDj&H-t*4?UamXX0peVspk"i_@ba+&u"J>UYDKV_^G,7V==aTZZ<YO7:sNSQ[6"Ja-29NtYjd#=`J@D'h+[QW=:EEb?A<k!f+\`g^?,Vgp7_)91[lR\f.Tkf7VIPLVYM&deF!aYt9Ip^"N",3F'*W~>endstream
endobj
11 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 209
>>
stream
Gap@G]+0EH(e/_@iZH]:>J`g!jPCLm;?AgU"fdk"PQZD\d?lRI_oWc[$tp^]O\:3fK8kWeX2&Jcg0+RoJ]j;2j*upu!b4.o&f)b$I@7CfIYjP^#\VjhC=QhQ]^lV-@<0Tam!0.+Dn@("AK%N,Uc7hb+6VoQ$q2q[7]BB92RoY/.j2N028i1jNf'@<1+Fqf$1&"8omHk`#DHP>OT~>endstream
endobj
xref
0 12
0000000000 65535 f
0000000073 00000 n
0000000104 00000 n
0000000211 00000 n
0000000404 00000 n
0000000598 00000 n
0000000792 00000 n
0000000860 00000 n
0000001156 00000 n
0000001227 00000 n
0000001526 00000 n
0000001826 00000 n
trailer
<<
/ID
[<80da26147a484f2b7573da8151a93d2e><80da26147a484f2b7573da8151a93d2e>]
% ReportLab generated PDF document -- digest (http://www.reportlab.com)
/Info 7 0 R
/Root 6 0 R
/Size 12
>>
startxref
2126
%%EOF

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,106 @@
%PDF-1.3
%“Œ‹ž ReportLab Generated PDF document http://www.reportlab.com
1 0 obj
<<
/F1 2 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/Contents 9 0 R /MediaBox [ 0 0 612 792 ] /Parent 8 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
4 0 obj
<<
/Contents 10 0 R /MediaBox [ 0 0 612 792 ] /Parent 8 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
5 0 obj
<<
/Contents 11 0 R /MediaBox [ 0 0 612 792 ] /Parent 8 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
6 0 obj
<<
/PageMode /UseNone /Pages 8 0 R /Type /Catalog
>>
endobj
7 0 obj
<<
/Author (anonymous) /CreationDate (D:20240718233034+00'00') /Creator (ReportLab PDF Library - www.reportlab.com) /Keywords () /ModDate (D:20240718233034+00'00') /Producer (ReportLab PDF Library - www.reportlab.com)
/Subject (unspecified) /Title (untitled) /Trapped /False
>>
endobj
8 0 obj
<<
/Count 3 /Kids [ 3 0 R 4 0 R 5 0 R ] /Type /Pages
>>
endobj
9 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 206
>>
stream
Gap@G\IO3f&4Lr[@S4&T2aReWZ3N'9",Ncra>5AuK^J(o@r?=EP>b]h[L@XZ8q7#[c:#H2:^/=b,p3^,&f-Q.'H%!U?%N\iVa1pLMlh/41\A8@dF5@0al:-1?L;D%LpL3g\9`.3c6N/Mp=sE/nO%^@%Cc3`]e`qqS@[pkUWemMZC<P\fkqa55u)*hIUoU437-gb!e_*&B/,&~>endstream
endobj
10 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 209
>>
stream
Gap@G\IO3V'LdA_ig"8P1PS=kA5Q_GQ\P]*S3\>Q`jHYt?8UdkV`6]UV*On)+1VMV+A@.iF:*6sWfM9f"s.NmVuMto!p7-+,Rb<.h,pdi-&OQ5KO\RRFj.j"A)ScTQ7$hudF^TnZ'XuQA5"O]rYkt><-DJmj'"Ri>n!4`^m409XX`e)AR'*rGsn6m79.18+^ba=qRuss"-A3k+9~>endstream
endobj
11 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 210
>>
stream
Gap@G]+0EH(e/_@iZH]:.1fBHK`Xl'[i1&AjX(\k8hbgo(QJ6R1Rsuo6_I1A5Gg$JL;D#$J2CX;+Cf*cUHk2%H1XmpWe+qZ5moJ#B]>b%%[d,mfSSkS4A:Q4NlOFfrL7eA,s45"eUSakM;927AA,1"-LZ)&nZ/ah=8_X7:?ZMj@J@;r7d`t]Z0\d39M%:$k8[S5D"2oSap4s80l?~>endstream
endobj
xref
0 12
0000000000 65535 f
0000000073 00000 n
0000000104 00000 n
0000000211 00000 n
0000000404 00000 n
0000000598 00000 n
0000000792 00000 n
0000000860 00000 n
0000001156 00000 n
0000001227 00000 n
0000001523 00000 n
0000001823 00000 n
trailer
<<
/ID
[<88edee24ee67bd7d6b7cf53cfa2222b0><88edee24ee67bd7d6b7cf53cfa2222b0>]
% ReportLab generated PDF document -- digest (http://www.reportlab.com)
/Info 7 0 R
/Root 6 0 R
/Size 12
>>
startxref
2124
%%EOF

View File

@@ -0,0 +1,106 @@
%PDF-1.3
%“Œ‹ž ReportLab Generated PDF document http://www.reportlab.com
1 0 obj
<<
/F1 2 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/Contents 9 0 R /MediaBox [ 0 0 612 792 ] /Parent 8 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
4 0 obj
<<
/Contents 10 0 R /MediaBox [ 0 0 612 792 ] /Parent 8 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
5 0 obj
<<
/Contents 11 0 R /MediaBox [ 0 0 612 792 ] /Parent 8 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
6 0 obj
<<
/PageMode /UseNone /Pages 8 0 R /Type /Catalog
>>
endobj
7 0 obj
<<
/Author (anonymous) /CreationDate (D:20240718233034+00'00') /Creator (ReportLab PDF Library - www.reportlab.com) /Keywords () /ModDate (D:20240718233034+00'00') /Producer (ReportLab PDF Library - www.reportlab.com)
/Subject (unspecified) /Title (untitled) /Trapped /False
>>
endobj
8 0 obj
<<
/Count 3 /Kids [ 3 0 R 4 0 R 5 0 R ] /Type /Pages
>>
endobj
9 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 209
>>
stream
Gap@GYmu@>'Ld5[if35rI0]sG)F[U^"c>T)"\\os-r:1V0,enq1Rsuo,*67.@k7U.LRF-P.e"CM2V!>iYi<g`nXh!K?n@$t^rY1$+^0'>=B8H6e;F1WmG#,(eS00(Qe9&:O@nI879DTsT,njXAB?`8:>,Hn3*RV!qh4;&@6%]<9Y*>QZ].Z5o;RAZXg7d[#+bphHs_Ep!QR2TZ2~>endstream
endobj
10 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 210
>>
stream
Gap@G]+0EH(e/_@iZH]:>=,iY1bE)XN?M;1'J/>i&HY;gks]*rj:!DKpb8@`prC#N+9E#o#-<G*!#p7e6j-1sX2k5S,6XmM"taYkfK^k">%usEeEk=sR<UT"dm`rXD;!S`_jS9LU+(R%e'V%WSMfHP.pXZEQqTQq=&D[I[PS(41(NIAZ1R/U?:Z=hSXu!NDF)bpG2F+/I/q/u1-Y~>endstream
endobj
11 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 209
>>
stream
Gap@G_$YcZ'LhbF`EQB$nqi=8S<;#HbK3&f>rnodRPo`Vf4P[3cJidY(I=[K5NWCT'<lHgci?oCRVNST&[k#q4oSC0FWgAt1pD4d_(hIRjn_Nt+cFgJlfm[1U8@/M4r^Pk<@F!@e?%/!-Vq;]nfdLi9]P2M)ck9?)%oNXa_\N<-d"(pjlH%-G`T@Sj&P(j6.@#Xh\Vr6!1iI2/H~>endstream
endobj
xref
0 12
0000000000 65535 f
0000000073 00000 n
0000000104 00000 n
0000000211 00000 n
0000000404 00000 n
0000000598 00000 n
0000000792 00000 n
0000000860 00000 n
0000001156 00000 n
0000001227 00000 n
0000001526 00000 n
0000001827 00000 n
trailer
<<
/ID
[<4fcc82a085fe71e34a32d1b23c8b939f><4fcc82a085fe71e34a32d1b23c8b939f>]
% ReportLab generated PDF document -- digest (http://www.reportlab.com)
/Info 7 0 R
/Root 6 0 R
/Size 12
>>
startxref
2127
%%EOF

View File

@@ -14,3 +14,8 @@ def after_scenario(context, scenario):
os.remove('response_file') os.remove('response_file')
if hasattr(context, 'file_name') and os.path.exists(context.file_name): if hasattr(context, 'file_name') and os.path.exists(context.file_name):
os.remove(context.file_name) os.remove(context.file_name)
# Remove any temporary files
for temp_file in os.listdir('.'):
if temp_file.startswith('genericNonCustomisableName') or temp_file.startswith('temp_image_'):
os.remove(temp_file)

View File

@@ -1,4 +1,4 @@
@example @example @general
Feature: API Validation Feature: API Validation
@positive @password @positive @password
@@ -92,10 +92,10 @@ Feature: API Validation
| threshold | 90 | | threshold | 90 |
| whitePercent | 99.9 | | whitePercent | 99.9 |
When I send the API request to the endpoint "/api/v1/misc/remove-blanks" When I send the API request to the endpoint "/api/v1/misc/remove-blanks"
Then the response content type should be "application/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 1 files
And the response file should have size greater than 0 And the response file should have size greater than 0
And the response PDF should contain 0 pages
And the response status code should be 200
@positive @flatten @positive @flatten
Scenario: Flatten PDF Scenario: Flatten PDF

View File

@@ -32,7 +32,7 @@ Feature: API Validation
@ocr @positive @ocr @positive
Scenario: Extract Image Scans Scenario: Extract Image Scans
Given I generate a PDF file as "fileInput" Given I generate a PDF file as "fileInput"
And the pdf contains 3 images on 2 pages And the pdf contains 3 images of size 300x300 on 2 pages
And the request data includes And the request data includes
| parameter | value | | parameter | value |
| angleThreshold | 5 | | angleThreshold | 5 |
@@ -125,8 +125,7 @@ Feature: API Validation
@ocr @ocr
Scenario: PDFA Scenario: PDFA
Given I generate a PDF file as "fileInput" Given I use an example file at "exampleFiles/pdfa2.pdf" as parameter "fileInput"
And the pdf contains 3 pages with random text
And the request data includes And the request data includes
| parameter | value | | parameter | value |
| outputFormat | pdfa | | outputFormat | pdfa |
@@ -137,8 +136,7 @@ Feature: API Validation
@ocr @ocr
Scenario: PDFA1 Scenario: PDFA1
Given I generate a PDF file as "fileInput" Given I use an example file at "exampleFiles/pdfa1.pdf" as parameter "fileInput"
And the pdf contains 3 pages with random text
And the request data includes And the request data includes
| parameter | value | | parameter | value |
| outputFormat | pdfa-1 | | outputFormat | pdfa-1 |
@@ -149,8 +147,7 @@ Feature: API Validation
@compress @ghostscript @positive @compress @ghostscript @positive
Scenario: Compress Scenario: Compress
Given I generate a PDF file as "fileInput" Given I use an example file at "exampleFiles/ghost3.pdf" as parameter "fileInput"
And the pdf contains 3 pages with random text
And the request data includes And the request data includes
| parameter | value | | parameter | value |
| optimizeLevel | 4 | | optimizeLevel | 4 |
@@ -161,8 +158,7 @@ Feature: API Validation
@compress @ghostscript @positive @compress @ghostscript @positive
Scenario: Compress Scenario: Compress
Given I generate a PDF file as "fileInput" Given I use an example file at "exampleFiles/ghost2.pdf" as parameter "fileInput"
And the pdf contains 3 pages with random text
And the request data includes And the request data includes
| parameter | value | | parameter | value |
| optimizeLevel | 1 | | optimizeLevel | 1 |
@@ -175,8 +171,7 @@ Feature: API Validation
@compress @ghostscript @positive @compress @ghostscript @positive
Scenario: Compress Scenario: Compress
Given I generate a PDF file as "fileInput" Given I use an example file at "exampleFiles/ghost1.pdf" as parameter "fileInput"
And the pdf contains 3 pages with random text
And the request data includes And the request data includes
| parameter | value | | parameter | value |
| optimizeLevel | 1 | | optimizeLevel | 1 |

View File

@@ -94,3 +94,23 @@ Feature: API Validation
| 1 | 10 | 2 | 10 | | 1 | 10 | 2 | 10 |
@extract-images
Scenario Outline: Extract Image Scans
Given I use an example file at "exampleFiles/images.pdf" as parameter "fileInput"
And the request data includes
| parameter | value |
| format | <format> |
When I send the API request to the endpoint "/api/v1/misc/extract-images"
Then the response content type should be "application/octet-stream"
And the response file should have extension ".zip"
And the response ZIP should contain 20 files
And the response file should have size greater than 0
And the response status code should be 200
Examples:
| format |
| png |
| gif |
| jpeg |

View File

@@ -6,11 +6,14 @@ import io
import random import random
import string import string
from reportlab.lib.pagesizes import letter from reportlab.lib.pagesizes import letter
from reportlab.lib.utils import ImageReader
from reportlab.pdfgen import canvas from reportlab.pdfgen import canvas
import mimetypes import mimetypes
import requests import requests
import zipfile import zipfile
import shutil import shutil
import re
from PIL import Image, ImageDraw
######### #########
# GIVEN # # GIVEN #
@@ -43,8 +46,6 @@ def step_use_example_file(context, filePath, fileInput):
except FileNotFoundError: except FileNotFoundError:
raise FileNotFoundError(f"The example file '{filePath}' does not exist.") raise FileNotFoundError(f"The example file '{filePath}' does not exist.")
@given('the pdf contains {page_count:d} pages') @given('the pdf contains {page_count:d} pages')
def step_pdf_contains_pages(context, page_count): def step_pdf_contains_pages(context, page_count):
writer = PdfWriter() writer = PdfWriter()
@@ -66,8 +67,6 @@ def step_pdf_contains_blank_pages(context, page_count):
context.files[context.param_name].close() context.files[context.param_name].close()
context.files[context.param_name] = open(context.file_name, 'rb') context.files[context.param_name] = open(context.file_name, 'rb')
def create_black_box_image(file_name, size): def create_black_box_image(file_name, size):
can = canvas.Canvas(file_name, pagesize=size) can = canvas.Canvas(file_name, pagesize=size)
width, height = size width, height = size
@@ -76,9 +75,25 @@ def create_black_box_image(file_name, size):
can.showPage() can.showPage()
can.save() can.save()
def create_pdf_with_black_boxes(file_name, image_count, page_count): @given(u'the pdf contains {image_count:d} images of size {width:d}x{height:d} on {page_count:d} pages')
page_width, page_height = letter def step_impl(context, image_count, width, height, page_count):
box_size = 72 # 1 inch by 1 inch black box context.param_name = "fileInput"
context.file_name = "genericNonCustomisableName.pdf"
create_pdf_with_images_and_boxes(context.file_name, image_count, page_count, width, height)
if not hasattr(context, 'files'):
context.files = {}
context.files[context.param_name] = open(context.file_name, 'rb')
def add_black_boxes_to_image(image):
if isinstance(image, str):
image = Image.open(image)
draw = ImageDraw.Draw(image)
draw.rectangle([(0, 0), image.size], fill=(0, 0, 0)) # Fill image with black
return image
def create_pdf_with_images_and_boxes(file_name, image_count, page_count, image_width, image_height):
page_width, page_height = max(letter[0], image_width), max(letter[1], image_height)
boxes_per_page = image_count // page_count + (1 if image_count % page_count != 0 else 0) boxes_per_page = image_count // page_count + (1 if image_count % page_count != 0 else 0)
writer = PdfWriter() writer = PdfWriter()
@@ -86,15 +101,31 @@ def create_pdf_with_black_boxes(file_name, image_count, page_count):
for page in range(page_count): for page in range(page_count):
packet = io.BytesIO() packet = io.BytesIO()
can = canvas.Canvas(packet, pagesize=letter) can = canvas.Canvas(packet, pagesize=(page_width, page_height))
for i in range(boxes_per_page): for i in range(boxes_per_page):
if box_counter >= image_count: if box_counter >= image_count:
break break
x = (i % (page_width // box_size)) * box_size
y = page_height - ((i // (page_width // box_size) + 1) * box_size) # Simulating a dynamic image creation (replace this with your actual image creation logic)
can.setFillColorRGB(0, 0, 0) # For demonstration, we'll create a simple black image
can.rect(x, y, box_size, box_size, fill=1) dummy_image = Image.new('RGB', (image_width, image_height), color='white') # Create a white image
dummy_image = add_black_boxes_to_image(dummy_image) # Add black boxes
# Convert the PIL Image to bytes to pass to drawImage
image_bytes = io.BytesIO()
dummy_image.save(image_bytes, format='PNG')
image_bytes.seek(0)
# Check if the image fits in the current page dimensions
x = (i % (page_width // image_width)) * image_width
y = page_height - (((i % (page_height // image_height)) + 1) * image_height)
if x + image_width > page_width or y < 0:
break
# Add the image to the PDF
can.drawImage(ImageReader(image_bytes), x, y, width=image_width, height=image_height)
box_counter += 1 box_counter += 1
can.showPage() can.showPage()
@@ -103,9 +134,16 @@ def create_pdf_with_black_boxes(file_name, image_count, page_count):
new_pdf = PdfReader(packet) new_pdf = PdfReader(packet)
writer.add_page(new_pdf.pages[0]) writer.add_page(new_pdf.pages[0])
# Write the PDF to file
with open(file_name, 'wb') as f: with open(file_name, 'wb') as f:
writer.write(f) writer.write(f)
# Clean up temporary image files
for i in range(image_count):
temp_image_path = f"temp_image_{i}.png"
if os.path.exists(temp_image_path):
os.remove(temp_image_path)
@given('the pdf contains {image_count:d} images on {page_count:d} pages') @given('the pdf contains {image_count:d} images on {page_count:d} pages')
def step_pdf_contains_images(context, image_count, page_count): def step_pdf_contains_images(context, image_count, page_count):
if not hasattr(context, 'param_name'): if not hasattr(context, 'param_name'):
@@ -118,7 +156,6 @@ def step_pdf_contains_images(context, image_count, page_count):
context.files[context.param_name].close() context.files[context.param_name].close()
context.files[context.param_name] = open(context.file_name, 'rb') context.files[context.param_name] = open(context.file_name, 'rb')
@given('the pdf contains {page_count:d} pages with random text') @given('the pdf contains {page_count:d} pages with random text')
def step_pdf_contains_pages_with_random_text(context, page_count): def step_pdf_contains_pages_with_random_text(context, page_count):
buffer = io.BytesIO() buffer = io.BytesIO()
@@ -186,6 +223,21 @@ def save_generated_pdf(context, filename):
# WHEN # # WHEN #
######## ########
@when('I send a GET request to "{endpoint}"')
def step_send_get_request(context, endpoint):
base_url = "http://localhost:8080"
full_url = f"{base_url}{endpoint}"
response = requests.get(full_url)
context.response = response
@when('I send a GET request to "{endpoint}" with parameters')
def step_send_get_request_with_params(context, endpoint):
base_url = "http://localhost:8080"
params = {row['parameter']: row['value'] for row in context.table}
full_url = f"{base_url}{endpoint}"
response = requests.get(full_url, params=params)
context.response = response
@when('I send the API request to the endpoint "{endpoint}"') @when('I send the API request to the endpoint "{endpoint}"')
def step_send_api_request(context, endpoint): def step_send_api_request(context, endpoint):
url = f"http://localhost:8080{endpoint}" url = f"http://localhost:8080{endpoint}"
@@ -278,7 +330,6 @@ def step_save_response_file(context, filename):
f.write(context.response.content) f.write(context.response.content)
print(f"Saved response content to {filename}") print(f"Saved response content to {filename}")
@then('the response PDF should contain {page_count:d} pages') @then('the response PDF should contain {page_count:d} pages')
def step_check_response_pdf_page_count(context, page_count): def step_check_response_pdf_page_count(context, page_count):
response_file = io.BytesIO(context.response.content) response_file = io.BytesIO(context.response.content)
@@ -305,3 +356,26 @@ def step_check_response_zip_doc_page_count(context, doc_count, pages_per_doc):
reader = PdfReader(pdf_file) reader = PdfReader(pdf_file)
actual_pages_per_doc = len(reader.pages) actual_pages_per_doc = len(reader.pages)
assert actual_pages_per_doc == pages_per_doc, f"Expected {pages_per_doc} pages per document but got {actual_pages_per_doc} pages in document {file_name}" assert actual_pages_per_doc == pages_per_doc, f"Expected {pages_per_doc} pages per document but got {actual_pages_per_doc} pages in document {file_name}"
@then('the JSON value of "{key}" should be "{expected_value}"')
def step_check_json_value(context, key, expected_value):
actual_value = context.response.json().get(key)
assert actual_value == expected_value, \
f"Expected JSON value for '{key}' to be '{expected_value}' but got '{actual_value}'"
@then('JSON list entry containing "{identifier_key}" as "{identifier_value}" should have "{target_key}" as "{target_value}"')
def step_check_json_list_entry(context, identifier_key, identifier_self, target_key, target_value):
json_response = context.response.json()
for entry in json_response:
if entry.get(identifier_key) == identifier_value:
assert entry.get(target_key) == target_value, \
f"Expected {target_key} to be {target_value} in entry where {identifier_key} is {identifier_value}, but found {entry.get(target_key)}"
break
else:
raise AssertionError(f"No entry with {identifier_key} as {identifier_value} found")
@then('the response should match the regex "{pattern}"')
def step_response_matches_regex(context, pattern):
response_text = context.response.text
assert re.match(pattern, response_text), \
f"Response '{response_text}' does not match the expected pattern '{pattern}'"

View File

@@ -22,7 +22,6 @@ services:
DOCKER_ENABLE_SECURITY: "false" DOCKER_ENABLE_SECURITY: "false"
SECURITY_ENABLELOGIN: "false" SECURITY_ENABLELOGIN: "false"
LANGS: "en_GB,en_US,ar_AR,de_DE,fr_FR,es_ES,zh_CN,zh_TW,ca_CA,it_IT,sv_SE,pl_PL,ro_RO,ko_KR,pt_BR,ru_RU,el_GR,hi_IN,hu_HU,tr_TR,id_ID" LANGS: "en_GB,en_US,ar_AR,de_DE,fr_FR,es_ES,zh_CN,zh_TW,ca_CA,it_IT,sv_SE,pl_PL,ro_RO,ko_KR,pt_BR,ru_RU,el_GR,hi_IN,hu_HU,tr_TR,id_ID"
INSTALL_BOOK_AND_ADVANCED_HTML_OPS: "true"
SYSTEM_DEFAULTLOCALE: en-US SYSTEM_DEFAULTLOCALE: en-US
UI_APPNAME: Stirling-PDF UI_APPNAME: Stirling-PDF
UI_HOMEDESCRIPTION: Demo site for Stirling-PDF Latest UI_HOMEDESCRIPTION: Demo site for Stirling-PDF Latest

View File

@@ -25,6 +25,11 @@ ignore = [
'text', 'text',
] ]
[da_DK]
ignore = [
'language.direction',
]
[de_DE] [de_DE]
ignore = [ ignore = [
'AddStampRequest.alphabet', 'AddStampRequest.alphabet',
@@ -87,6 +92,11 @@ ignore = [
'watermark.type.2', 'watermark.type.2',
] ]
[ga_IE]
ignore = [
'language.direction',
]
[hi_IN] [hi_IN]
ignore = [ ignore = [
'language.direction', 'language.direction',
@@ -171,7 +181,9 @@ ignore = [
[pt_BR] [pt_BR]
ignore = [ ignore = [
'changeMetadata.trapped',
'language.direction', 'language.direction',
'pipelineOptions.pipelineHeader',
] ]
[pt_PT] [pt_PT]
@@ -212,6 +224,14 @@ ignore = [
'language.direction', 'language.direction',
] ]
[th_TH]
ignore = [
'language.direction',
'pipeline.title',
'pipelineOptions.pipelineHeader',
'showJS.tags',
]
[tr_TR] [tr_TR]
ignore = [ ignore = [
'language.direction', 'language.direction',
@@ -222,6 +242,14 @@ ignore = [
'language.direction', 'language.direction',
] ]
[vi_VN]
ignore = [
'language.direction',
'pipeline.title',
'pipelineOptions.pipelineHeader',
'showJS.tags',
]
[zh_CN] [zh_CN]
ignore = [ ignore = [
'language.direction', 'language.direction',

View File

@@ -12,7 +12,8 @@ fi
umask "$UMASK" || true umask "$UMASK" || true
if [[ "$INSTALL_BOOK_AND_ADVANCED_HTML_OPS" == "true" && "$FAT_DOCKER" != "true" ]]; then if [[ "$INSTALL_BOOK_AND_ADVANCED_HTML_OPS" == "true" && "$FAT_DOCKER" != "true" ]]; then
apk add --no-cache calibre@testing echo "issue with calibre in current version, feature currently disabled on Stirling-PDF"
#apk add --no-cache calibre@testing
fi fi
if [[ "$FAT_DOCKER" != "true" ]]; then if [[ "$FAT_DOCKER" != "true" ]]; then

174
scripts/png_to_webp.py Normal file
View File

@@ -0,0 +1,174 @@
"""
Author: Ludy87
Description: This script converts a PDF file to WebP images. It includes functionality to resize images if they exceed specified dimensions and handle conversion of PDF pages to WebP format.
Example
-------
To convert a PDF file to WebP images with each page as a separate WebP file:
python script.py input.pdf output_directory
To convert a PDF file to a single WebP image:
python script.py input.pdf output_directory --single
To adjust the DPI resolution for rendering PDF pages:
python script.py input.pdf output_directory --dpi 150
"""
import argparse
import os
from pdf2image import convert_from_path
from PIL import Image
def resize_image(input_image_path, output_image_path, max_size=(16383, 16383)):
"""
Resize the image if its dimensions exceed the maximum allowed size and save it as WebP.
Parameters
----------
input_image_path : str
Path to the input image file.
output_image_path : str
Path where the output WebP image will be saved.
max_size : tuple of int, optional
Maximum allowed dimensions for the image (width, height). Default is (16383, 16383).
Returns
-------
None
"""
try:
# Open the image
image = Image.open(input_image_path)
width, height = image.size
max_width, max_height = max_size
# Check if the image dimensions exceed the maximum allowed dimensions
if width > max_width or height > max_height:
# Calculate the scaling ratio
ratio = min(max_width / width, max_height / height)
new_width = int(width * ratio)
new_height = int(height * ratio)
# Resize the image
resized_image = image.resize((new_width, new_height), Image.LANCZOS)
resized_image.save(output_image_path, format="WEBP", quality=100)
print(
f"The image was successfully resized to ({new_width}, {new_height}) and saved as WebP: {output_image_path}"
)
else:
# If dimensions are within the allowed limits, save the image directly
image.save(output_image_path, format="WEBP", quality=100)
print(f"The image was successfully saved as WebP: {output_image_path}")
except Exception as e:
print(f"An error occurred: {e}")
def convert_image_to_webp(input_image, output_file):
"""
Convert an image to WebP format, resizing it if it exceeds the maximum dimensions.
Parameters
----------
input_image : str
Path to the input image file.
output_file : str
Path where the output WebP image will be saved.
Returns
-------
None
"""
# Resize the image if it exceeds the maximum dimensions
resize_image(input_image, output_file, max_size=(16383, 16383))
def pdf_to_webp(pdf_path, output_dir, dpi=300):
"""
Convert each page of a PDF file to WebP images.
Parameters
----------
pdf_path : str
Path to the input PDF file.
output_dir : str
Directory where the WebP images will be saved.
dpi : int, optional
DPI resolution for rendering PDF pages. Default is 300.
Returns
-------
None
"""
# Convert the PDF to a list of images
images = convert_from_path(pdf_path, dpi=dpi)
for page_number, image in enumerate(images):
# Define temporary PNG path
temp_png_path = os.path.join(output_dir, f"temp_page_{page_number + 1}.png")
image.save(temp_png_path, format="PNG")
# Define the output path for WebP
output_path = os.path.join(output_dir, f"page_{page_number + 1}.webp")
# Convert PNG to WebP
convert_image_to_webp(temp_png_path, output_path)
# Delete the temporary PNG file
os.remove(temp_png_path)
def main(pdf_image_path, output_dir, dpi=300, single_images_flag=False):
"""
Main function to handle conversion from PDF to WebP images.
Parameters
----------
pdf_image_path : str
Path to the input PDF file or image.
output_dir : str
Directory where the WebP images will be saved.
dpi : int, optional
DPI resolution for rendering PDF pages. Default is 300.
single_images_flag : bool, optional
If True, combine all pages into a single WebP image. Default is False.
Returns
-------
None
"""
if single_images_flag:
# Combine all pages into a single WebP image
output_path = os.path.join(output_dir, "combined_image.webp")
convert_image_to_webp(pdf_image_path, output_path)
else:
# Convert each PDF page to a separate WebP image
pdf_to_webp(pdf_image_path, output_dir, dpi)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Convert a PDF file to WebP images.")
parser.add_argument("pdf_path", help="The path to the input PDF file.")
parser.add_argument(
"output_dir", help="The directory where the WebP images should be saved."
)
parser.add_argument(
"--dpi",
type=int,
default=300,
help="The DPI resolution for rendering the PDF pages (default: 300).",
)
parser.add_argument(
"--single",
action="store_true",
help="Combine all pages into a single WebP image.",
)
args = parser.parse_args()
os.makedirs(args.output_dir, exist_ok=True)
main(
args.pdf_path,
args.output_dir,
dpi=args.dpi,
single_images_flag=args.single,
)

View File

@@ -45,7 +45,6 @@ public class SPdfApplication {
// Check if the BROWSER_OPEN environment variable is set to true // Check if the BROWSER_OPEN environment variable is set to true
String browserOpenEnv = env.getProperty("BROWSER_OPEN"); String browserOpenEnv = env.getProperty("BROWSER_OPEN");
boolean browserOpen = browserOpenEnv != null && "true".equalsIgnoreCase(browserOpenEnv); boolean browserOpen = browserOpenEnv != null && "true".equalsIgnoreCase(browserOpenEnv);
if (browserOpen) { if (browserOpen) {
try { try {
String url = "http://localhost:" + getNonStaticPort(); String url = "http://localhost:" + getNonStaticPort();
@@ -79,13 +78,14 @@ public class SPdfApplication {
// custom javs settings file // custom javs settings file
if (Files.exists(Paths.get("configs/custom_settings.yml"))) { if (Files.exists(Paths.get("configs/custom_settings.yml"))) {
String existing = propertyFiles.getOrDefault("spring.config.additional-location", ""); String existingLocation =
if (!existing.isEmpty()) { propertyFiles.getOrDefault("spring.config.additional-location", "");
existing += ","; if (!existingLocation.isEmpty()) {
existingLocation += ",";
} }
propertyFiles.put( propertyFiles.put(
"spring.config.additional-location", "spring.config.additional-location",
existing + "file:configs/custom_settings.yml"); existingLocation + "file:configs/custom_settings.yml");
} else { } else {
logger.warn("Custom configuration file 'configs/custom_settings.yml' does not exist."); logger.warn("Custom configuration file 'configs/custom_settings.yml' does not exist.");
} }

View File

@@ -32,25 +32,25 @@ public class CleanUrlInterceptor implements HandlerInterceptor {
String queryString = request.getQueryString(); String queryString = request.getQueryString();
if (queryString != null && !queryString.isEmpty()) { if (queryString != null && !queryString.isEmpty()) {
String requestURI = request.getRequestURI(); String requestURI = request.getRequestURI();
Map<String, String> parameters = new HashMap<>(); Map<String, String> allowedParameters = new HashMap<>();
// Keep only the allowed parameters // Keep only the allowed parameters
String[] queryParameters = queryString.split("&"); String[] queryParameters = queryString.split("&");
for (String param : queryParameters) { for (String param : queryParameters) {
String[] keyValue = param.split("="); String[] keyValuePair = param.split("=");
if (keyValue.length != 2) { if (keyValuePair.length != 2) {
continue; continue;
} }
if (ALLOWED_PARAMS.contains(keyValue[0])) { if (ALLOWED_PARAMS.contains(keyValuePair[0])) {
parameters.put(keyValue[0], keyValue[1]); allowedParameters.put(keyValuePair[0], keyValuePair[1]);
} }
} }
// If there are any parameters that are not allowed // If there are any parameters that are not allowed
if (parameters.size() != queryParameters.length) { if (allowedParameters.size() != queryParameters.length) {
// Construct new query string // Construct new query string
StringBuilder newQueryString = new StringBuilder(); StringBuilder newQueryString = new StringBuilder();
for (Map.Entry<String, String> entry : parameters.entrySet()) { for (Map.Entry<String, String> entry : allowedParameters.entrySet()) {
if (newQueryString.length() > 0) { if (newQueryString.length() > 0) {
newQueryString.append("&"); newQueryString.append("&");
} }

View File

@@ -14,6 +14,8 @@ import java.util.List;
import org.simpleyaml.configuration.comments.CommentType; import org.simpleyaml.configuration.comments.CommentType;
import org.simpleyaml.configuration.file.YamlFile; import org.simpleyaml.configuration.file.YamlFile;
import org.simpleyaml.configuration.implementation.SimpleYamlImplementation;
import org.simpleyaml.configuration.implementation.snakeyaml.lib.DumperOptions;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContextInitializer; import org.springframework.context.ApplicationContextInitializer;
@@ -71,9 +73,17 @@ public class ConfigInitializer
} }
final YamlFile settingsTemplateFile = new YamlFile(tempTemplatePath.toFile()); final YamlFile settingsTemplateFile = new YamlFile(tempTemplatePath.toFile());
DumperOptions yamlOptionsSettingsTemplateFile =
((SimpleYamlImplementation) settingsTemplateFile.getImplementation())
.getDumperOptions();
yamlOptionsSettingsTemplateFile.setSplitLines(false);
settingsTemplateFile.loadWithComments(); settingsTemplateFile.loadWithComments();
final YamlFile settingsFile = new YamlFile(settingsPath.toFile()); final YamlFile settingsFile = new YamlFile(settingsPath.toFile());
DumperOptions yamlOptionsSettingsFile =
((SimpleYamlImplementation) settingsFile.getImplementation())
.getDumperOptions();
yamlOptionsSettingsFile.setSplitLines(false);
settingsFile.loadWithComments(); settingsFile.loadWithComments();
// Load headers and comments // Load headers and comments
@@ -81,6 +91,10 @@ public class ConfigInitializer
// Create a new file for temporary settings // Create a new file for temporary settings
final YamlFile tempSettingFile = new YamlFile(settingsPath.toFile()); final YamlFile tempSettingFile = new YamlFile(settingsPath.toFile());
DumperOptions yamlOptionsTempSettingFile =
((SimpleYamlImplementation) tempSettingFile.getImplementation())
.getDumperOptions();
yamlOptionsTempSettingFile.setSplitLines(false);
tempSettingFile.createNewFile(true); tempSettingFile.createNewFile(true);
tempSettingFile.setHeader(header); tempSettingFile.setHeader(header);

View File

@@ -137,6 +137,7 @@ public class EndpointConfiguration {
addEndpointToGroup("Other", "auto-rename"); addEndpointToGroup("Other", "auto-rename");
addEndpointToGroup("Other", "get-info-on-pdf"); addEndpointToGroup("Other", "get-info-on-pdf");
addEndpointToGroup("Other", "show-javascript"); addEndpointToGroup("Other", "show-javascript");
addEndpointToGroup("Other", "remove-image-pdf");
// CLI // CLI
addEndpointToGroup("CLI", "compress-pdf"); addEndpointToGroup("CLI", "compress-pdf");
@@ -165,6 +166,7 @@ public class EndpointConfiguration {
addEndpointToGroup("Python", REMOVE_BLANKS); addEndpointToGroup("Python", REMOVE_BLANKS);
addEndpointToGroup("Python", "html-to-pdf"); addEndpointToGroup("Python", "html-to-pdf");
addEndpointToGroup("Python", "url-to-pdf"); addEndpointToGroup("Python", "url-to-pdf");
addEndpointToGroup("Python", "pdf-to-img");
// openCV // openCV
addEndpointToGroup("OpenCV", "extract-image-scans"); addEndpointToGroup("OpenCV", "extract-image-scans");
@@ -221,6 +223,7 @@ public class EndpointConfiguration {
addEndpointToGroup("Java", "split-pdf-by-sections"); addEndpointToGroup("Java", "split-pdf-by-sections");
addEndpointToGroup("Java", REMOVE_BLANKS); addEndpointToGroup("Java", REMOVE_BLANKS);
addEndpointToGroup("Java", "pdf-to-text"); addEndpointToGroup("Java", "pdf-to-text");
addEndpointToGroup("Java", "remove-image-pdf");
// Javascript // Javascript
addEndpointToGroup("Javascript", "pdf-organizer"); addEndpointToGroup("Javascript", "pdf-organizer");

View File

@@ -3,9 +3,8 @@ package stirling.software.SPDF.config.security;
import java.io.IOException; import java.io.IOException;
import java.util.Optional; import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.InternalAuthenticationServiceException; import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.authentication.LockedException; import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.AuthenticationException;
@@ -15,17 +14,16 @@ import org.springframework.security.web.authentication.SimpleUrlAuthenticationFa
import jakarta.servlet.ServletException; import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.User; import stirling.software.SPDF.model.User;
@Slf4j
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler { public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
private LoginAttemptService loginAttemptService; private LoginAttemptService loginAttemptService;
private UserService userService; private UserService userService;
private static final Logger logger =
LoggerFactory.getLogger(CustomAuthenticationFailureHandler.class);
public CustomAuthenticationFailureHandler( public CustomAuthenticationFailureHandler(
final LoginAttemptService loginAttemptService, UserService userService) { final LoginAttemptService loginAttemptService, UserService userService) {
this.loginAttemptService = loginAttemptService; this.loginAttemptService = loginAttemptService;
@@ -39,14 +37,17 @@ public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationF
AuthenticationException exception) AuthenticationException exception)
throws IOException, ServletException { throws IOException, ServletException {
if (exception instanceof DisabledException) {
log.error("User is deactivated: ", exception);
getRedirectStrategy().sendRedirect(request, response, "/logout?userIsDisabled=true");
return;
}
String ip = request.getRemoteAddr(); String ip = request.getRemoteAddr();
logger.error("Failed login attempt from IP: {}", ip); log.error("Failed login attempt from IP: {}", ip);
String contextPath = request.getContextPath(); if (exception instanceof LockedException) {
getRedirectStrategy().sendRedirect(request, response, "/login?error=locked");
if (exception.getClass().isAssignableFrom(InternalAuthenticationServiceException.class)
|| "Password must not be null".equalsIgnoreCase(exception.getMessage())) {
response.sendRedirect(contextPath + "/login?error=oauth2AuthenticationError");
return; return;
} }
@@ -54,20 +55,25 @@ public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationF
Optional<User> optUser = userService.findByUsernameIgnoreCase(username); Optional<User> optUser = userService.findByUsernameIgnoreCase(username);
if (username != null && optUser.isPresent() && !isDemoUser(optUser)) { if (username != null && optUser.isPresent() && !isDemoUser(optUser)) {
logger.info( log.info(
"Remaining attempts for user {}: {}", "Remaining attempts for user {}: {}",
optUser.get().getUsername(), username,
loginAttemptService.getRemainingAttempts(username)); loginAttemptService.getRemainingAttempts(username));
loginAttemptService.loginFailed(username); loginAttemptService.loginFailed(username);
if (loginAttemptService.isBlocked(username) if (loginAttemptService.isBlocked(username) || exception instanceof LockedException) {
|| exception.getClass().isAssignableFrom(LockedException.class)) { getRedirectStrategy().sendRedirect(request, response, "/login?error=locked");
response.sendRedirect(contextPath + "/login?error=locked");
return; return;
} }
} }
if (exception.getClass().isAssignableFrom(BadCredentialsException.class) if (exception instanceof BadCredentialsException
|| exception.getClass().isAssignableFrom(UsernameNotFoundException.class)) { || exception instanceof UsernameNotFoundException) {
response.sendRedirect(contextPath + "/login?error=badcredentials"); getRedirectStrategy().sendRedirect(request, response, "/login?error=badcredentials");
return;
}
if (exception instanceof InternalAuthenticationServiceException
|| "Password must not be null".equalsIgnoreCase(exception.getMessage())) {
getRedirectStrategy()
.sendRedirect(request, response, "/login?error=oauth2AuthenticationError");
return; return;
} }

View File

@@ -10,15 +10,20 @@ import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession; import jakarta.servlet.http.HttpSession;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.utils.RequestUriUtils; import stirling.software.SPDF.utils.RequestUriUtils;
@Slf4j
public class CustomAuthenticationSuccessHandler public class CustomAuthenticationSuccessHandler
extends SavedRequestAwareAuthenticationSuccessHandler { extends SavedRequestAwareAuthenticationSuccessHandler {
private LoginAttemptService loginAttemptService; private LoginAttemptService loginAttemptService;
private UserService userService;
public CustomAuthenticationSuccessHandler(LoginAttemptService loginAttemptService) { public CustomAuthenticationSuccessHandler(
LoginAttemptService loginAttemptService, UserService userService) {
this.loginAttemptService = loginAttemptService; this.loginAttemptService = loginAttemptService;
this.userService = userService;
} }
@Override @Override
@@ -27,6 +32,10 @@ public class CustomAuthenticationSuccessHandler
throws ServletException, IOException { throws ServletException, IOException {
String userName = request.getParameter("username"); String userName = request.getParameter("username");
if (userService.isUserDisabled(userName)) {
getRedirectStrategy().sendRedirect(request, response, "/logout?userIsDisabled=true");
return;
}
loginAttemptService.loginSucceeded(userName); loginAttemptService.loginSucceeded(userName);
// Get the saved request // Get the saved request

View File

@@ -2,32 +2,26 @@ package stirling.software.SPDF.config.security;
import java.io.IOException; import java.io.IOException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler; import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
import jakarta.servlet.ServletException; import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler { public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
@Autowired SessionRegistry sessionRegistry;
@Override @Override
public void onLogoutSuccess( public void onLogoutSuccess(
HttpServletRequest request, HttpServletResponse response, Authentication authentication) HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException, ServletException { throws IOException, ServletException {
HttpSession session = request.getSession(false);
if (session != null) { if (request.getParameter("userIsDisabled") != null) {
String sessionId = session.getId(); getRedirectStrategy()
sessionRegistry.removeSessionInformation(sessionId); .sendRedirect(request, response, "/login?erroroauth=userIsDisabled");
session.invalidate(); return;
logger.debug("Session invalidated: " + sessionId);
} }
response.sendRedirect(request.getContextPath() + "/login?logout=true"); getRedirectStrategy().sendRedirect(request, response, "/login?logout=true");
} }
} }

View File

@@ -6,6 +6,8 @@ import java.nio.file.Paths;
import java.util.UUID; import java.util.UUID;
import org.simpleyaml.configuration.file.YamlFile; import org.simpleyaml.configuration.file.YamlFile;
import org.simpleyaml.configuration.implementation.SimpleYamlImplementation;
import org.simpleyaml.configuration.implementation.snakeyaml.lib.DumperOptions;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@@ -92,6 +94,9 @@ public class InitialSecuritySetup {
Path path = Paths.get("configs", "settings.yml"); // Target the configs/settings.yml Path path = Paths.get("configs", "settings.yml"); // Target the configs/settings.yml
final YamlFile settingsYml = new YamlFile(path.toFile()); final YamlFile settingsYml = new YamlFile(path.toFile());
DumperOptions yamlOptionssettingsYml =
((SimpleYamlImplementation) settingsYml.getImplementation()).getDumperOptions();
yamlOptionssettingsYml.setSplitLines(false);
settingsYml.loadWithComments(); settingsYml.loadWithComments();

View File

@@ -3,8 +3,6 @@ package stirling.software.SPDF.config.security;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -17,8 +15,6 @@ public class LoginAttemptService {
@Autowired ApplicationProperties applicationProperties; @Autowired ApplicationProperties applicationProperties;
private static final Logger logger = LoggerFactory.getLogger(LoginAttemptService.class);
private int MAX_ATTEMPT; private int MAX_ATTEMPT;
private long ATTEMPT_INCREMENT_TIME; private long ATTEMPT_INCREMENT_TIME;
private ConcurrentHashMap<String, AttemptCounter> attemptsCache; private ConcurrentHashMap<String, AttemptCounter> attemptsCache;

View File

@@ -18,8 +18,6 @@ import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.client.registration.ClientRegistration;
@@ -37,6 +35,7 @@ import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationF
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationSuccessHandler; import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationSuccessHandler;
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2LogoutSuccessHandler; import stirling.software.SPDF.config.security.oauth2.CustomOAuth2LogoutSuccessHandler;
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2UserService; import stirling.software.SPDF.config.security.oauth2.CustomOAuth2UserService;
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2; import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2.Client; import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2.Client;
@@ -47,7 +46,7 @@ import stirling.software.SPDF.model.provider.KeycloakProvider;
import stirling.software.SPDF.repository.JPATokenRepositoryImpl; import stirling.software.SPDF.repository.JPATokenRepositoryImpl;
@Configuration @Configuration
@EnableWebSecurity() @EnableWebSecurity
@EnableMethodSecurity @EnableMethodSecurity
public class SecurityConfiguration { public class SecurityConfiguration {
@@ -73,11 +72,7 @@ public class SecurityConfiguration {
@Autowired private LoginAttemptService loginAttemptService; @Autowired private LoginAttemptService loginAttemptService;
@Autowired private FirstLoginFilter firstLoginFilter; @Autowired private FirstLoginFilter firstLoginFilter;
@Autowired private SessionPersistentRegistry sessionRegistry;
@Bean
public SessionRegistry sessionRegistry() {
return new SessionRegistryImpl();
}
@Bean @Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
@@ -94,7 +89,7 @@ public class SecurityConfiguration {
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.maximumSessions(10) .maximumSessions(10)
.maxSessionsPreventsLogin(false) .maxSessionsPreventsLogin(false)
.sessionRegistry(sessionRegistry()) .sessionRegistry(sessionRegistry)
.expiredUrl("/login?logout=true")); .expiredUrl("/login?logout=true"));
http.formLogin( http.formLogin(
@@ -103,7 +98,7 @@ public class SecurityConfiguration {
.loginPage("/login") .loginPage("/login")
.successHandler( .successHandler(
new CustomAuthenticationSuccessHandler( new CustomAuthenticationSuccessHandler(
loginAttemptService)) loginAttemptService, userService))
.defaultSuccessUrl("/") .defaultSuccessUrl("/")
.failureHandler( .failureHandler(
new CustomAuthenticationFailureHandler( new CustomAuthenticationFailureHandler(
@@ -160,7 +155,11 @@ public class SecurityConfiguration {
// Handle OAUTH2 Logins // Handle OAUTH2 Logins
if (applicationProperties.getSecurity().getOAUTH2() != null if (applicationProperties.getSecurity().getOAUTH2() != null
&& applicationProperties.getSecurity().getOAUTH2().getEnabled()) { && applicationProperties.getSecurity().getOAUTH2().getEnabled()
&& !applicationProperties
.getSecurity()
.getLoginMethod()
.equalsIgnoreCase("normal")) {
http.oauth2Login( http.oauth2Login(
oauth2 -> oauth2 ->
@@ -192,9 +191,7 @@ public class SecurityConfiguration {
logout -> logout ->
logout.logoutSuccessHandler( logout.logoutSuccessHandler(
new CustomOAuth2LogoutSuccessHandler( new CustomOAuth2LogoutSuccessHandler(
this.applicationProperties, applicationProperties)));
sessionRegistry()))
.invalidateHttpSession(true));
} }
} else { } else {
http.csrf(csrf -> csrf.disable()) http.csrf(csrf -> csrf.disable())

View File

@@ -1,6 +1,9 @@
package stirling.software.SPDF.config.security; package stirling.software.SPDF.config.security;
import java.io.IOException; import java.io.IOException;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Qualifier;
@@ -8,9 +11,11 @@ import org.springframework.context.annotation.Lazy;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.session.SessionInformation;
import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.filter.OncePerRequestFilter;
@@ -18,15 +23,17 @@ import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException; import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
import stirling.software.SPDF.model.ApiKeyAuthenticationToken; import stirling.software.SPDF.model.ApiKeyAuthenticationToken;
import stirling.software.SPDF.model.User;
@Component @Component
public class UserAuthenticationFilter extends OncePerRequestFilter { public class UserAuthenticationFilter extends OncePerRequestFilter {
@Autowired private UserDetailsService userDetailsService;
@Autowired @Lazy private UserService userService; @Autowired @Lazy private UserService userService;
@Autowired private SessionPersistentRegistry sessionPersistentRegistry;
@Autowired @Autowired
@Qualifier("loginEnabled") @Qualifier("loginEnabled")
public boolean loginEnabledValue; public boolean loginEnabledValue;
@@ -51,15 +58,20 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
try { try {
// Use API key to authenticate. This requires you to have an authentication // Use API key to authenticate. This requires you to have an authentication
// provider for API keys. // provider for API keys.
UserDetails userDetails = userService.loadUserByApiKey(apiKey); Optional<User> user = userService.loadUserByApiKey(apiKey);
if (userDetails == null) { if (!user.isPresent()) {
response.setStatus(HttpStatus.UNAUTHORIZED.value()); response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().write("Invalid API Key."); response.getWriter().write("Invalid API Key.");
return; return;
} }
authentication = List<SimpleGrantedAuthority> authorities =
new ApiKeyAuthenticationToken( user.get().getAuthorities().stream()
userDetails, apiKey, userDetails.getAuthorities()); .map(
authority ->
new SimpleGrantedAuthority(
authority.getAuthority()))
.collect(Collectors.toList());
authentication = new ApiKeyAuthenticationToken(user.get(), apiKey, authorities);
SecurityContextHolder.getContext().setAuthentication(authentication); SecurityContextHolder.getContext().setAuthentication(authentication);
} catch (AuthenticationException e) { } catch (AuthenticationException e) {
// If API key authentication fails, deny the request // If API key authentication fails, deny the request
@@ -87,6 +99,43 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
} }
} }
// Check if the authenticated user is disabled and invalidate their session if so
if (authentication != null && authentication.isAuthenticated()) {
Object principal = authentication.getPrincipal();
String username = null;
if (principal instanceof UserDetails) {
username = ((UserDetails) principal).getUsername();
} else if (principal instanceof OAuth2User) {
username = ((OAuth2User) principal).getName();
} else if (principal instanceof String) {
username = (String) principal;
}
List<SessionInformation> sessionsInformations =
sessionPersistentRegistry.getAllSessions(principal, false);
if (username != null) {
boolean isUserExists = userService.usernameExistsIgnoreCase(username);
boolean isUserDisabled = userService.isUserDisabled(username);
if (!isUserExists || isUserDisabled) {
for (SessionInformation sessionsInformation : sessionsInformations) {
sessionsInformation.expireNow();
sessionPersistentRegistry.expireSession(sessionsInformation.getSessionId());
}
}
if (!isUserExists) {
response.sendRedirect(request.getContextPath() + "/logout?badcredentials=true");
return;
}
if (isUserDisabled) {
response.sendRedirect(request.getContextPath() + "/logout?userIsDisabled=true");
return;
}
}
}
filterChain.doFilter(request, response); filterChain.doFilter(request, response);
} }

View File

@@ -15,12 +15,16 @@ import org.springframework.security.authentication.UsernamePasswordAuthenticatio
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.session.SessionInformation;
import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.DatabaseBackupInterface; import stirling.software.SPDF.config.DatabaseBackupInterface;
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
import stirling.software.SPDF.controller.api.pipeline.UserServiceInterface; import stirling.software.SPDF.controller.api.pipeline.UserServiceInterface;
import stirling.software.SPDF.model.AuthenticationType; import stirling.software.SPDF.model.AuthenticationType;
import stirling.software.SPDF.model.Authority; import stirling.software.SPDF.model.Authority;
@@ -40,6 +44,8 @@ public class UserService implements UserServiceInterface {
@Autowired private MessageSource messageSource; @Autowired private MessageSource messageSource;
@Autowired private SessionPersistentRegistry sessionRegistry;
@Autowired DatabaseBackupInterface databaseBackupHelper; @Autowired DatabaseBackupInterface databaseBackupHelper;
// Handle OAUTH2 login and user auto creation. // Handle OAUTH2 login and user auto creation.
@@ -48,7 +54,7 @@ public class UserService implements UserServiceInterface {
if (!isUsernameValid(username)) { if (!isUsernameValid(username)) {
return false; return false;
} }
Optional<User> existingUser = userRepository.findByUsernameIgnoreCase(username); Optional<User> existingUser = findByUsernameIgnoreCase(username);
if (existingUser.isPresent()) { if (existingUser.isPresent()) {
return true; return true;
} }
@@ -60,8 +66,8 @@ public class UserService implements UserServiceInterface {
} }
public Authentication getAuthentication(String apiKey) { public Authentication getAuthentication(String apiKey) {
User user = getUserByApiKey(apiKey); Optional<User> user = getUserByApiKey(apiKey);
if (user == null) { if (!user.isPresent()) {
throw new UsernameNotFoundException("API key is not valid"); throw new UsernameNotFoundException("API key is not valid");
} }
@@ -69,7 +75,7 @@ public class UserService implements UserServiceInterface {
return new UsernamePasswordAuthenticationToken( return new UsernamePasswordAuthenticationToken(
user, // principal (typically the user) user, // principal (typically the user)
null, // credentials (we don't expose the password or API key here) null, // credentials (we don't expose the password or API key here)
getAuthorities(user) // user's authorities (roles/permissions) getAuthorities(user.get()) // user's authorities (roles/permissions)
); );
} }
@@ -84,18 +90,17 @@ public class UserService implements UserServiceInterface {
String apiKey; String apiKey;
do { do {
apiKey = UUID.randomUUID().toString(); apiKey = UUID.randomUUID().toString();
} while (userRepository.findByApiKey(apiKey) != null); // Ensure uniqueness } while (userRepository.findByApiKey(apiKey).isPresent()); // Ensure uniqueness
return apiKey; return apiKey;
} }
public User addApiKeyToUser(String username) { public User addApiKeyToUser(String username) {
User user = Optional<User> user = findByUsernameIgnoreCase(username);
userRepository if (user.isPresent()) {
.findByUsernameIgnoreCase(username) user.get().setApiKey(generateApiKey());
.orElseThrow(() -> new UsernameNotFoundException("User not found")); return userRepository.save(user.get());
}
user.setApiKey(generateApiKey()); throw new UsernameNotFoundException("User not found");
return userRepository.save(user);
} }
public User refreshApiKeyForUser(String username) { public User refreshApiKeyForUser(String username) {
@@ -104,39 +109,40 @@ public class UserService implements UserServiceInterface {
public String getApiKeyForUser(String username) { public String getApiKeyForUser(String username) {
User user = User user =
userRepository findByUsernameIgnoreCase(username)
.findByUsernameIgnoreCase(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found")); .orElseThrow(() -> new UsernameNotFoundException("User not found"));
return user.getApiKey(); return user.getApiKey();
} }
public boolean isValidApiKey(String apiKey) { public boolean isValidApiKey(String apiKey) {
return userRepository.findByApiKey(apiKey) != null; return userRepository.findByApiKey(apiKey).isPresent();
} }
public User getUserByApiKey(String apiKey) { public Optional<User> getUserByApiKey(String apiKey) {
return userRepository.findByApiKey(apiKey); return userRepository.findByApiKey(apiKey);
} }
public UserDetails loadUserByApiKey(String apiKey) { public Optional<User> loadUserByApiKey(String apiKey) {
User user = userRepository.findByApiKey(apiKey); Optional<User> user = userRepository.findByApiKey(apiKey);
if (user != null) {
// Convert your User entity to a UserDetails object with authorities if (user.isPresent()) {
return new org.springframework.security.core.userdetails.User( return user;
user.getUsername(),
user.getPassword(), // you might not need this for API key auth
getAuthorities(user));
} }
return null; // or throw an exception return null; // or throw an exception
} }
public boolean validateApiKeyForUser(String username, String apiKey) { public boolean validateApiKeyForUser(String username, String apiKey) {
Optional<User> userOpt = userRepository.findByUsernameIgnoreCase(username); Optional<User> userOpt = findByUsernameIgnoreCase(username);
return userOpt.isPresent() && apiKey.equals(userOpt.get().getApiKey()); return userOpt.isPresent() && apiKey.equals(userOpt.get().getApiKey());
} }
public void saveUser(String username, AuthenticationType authenticationType) public void saveUser(String username, AuthenticationType authenticationType)
throws IllegalArgumentException, IOException { throws IllegalArgumentException, IOException {
saveUser(username, authenticationType, Role.USER.getRoleId());
}
public void saveUser(String username, AuthenticationType authenticationType, String role)
throws IllegalArgumentException, IOException {
if (!isUsernameValid(username)) { if (!isUsernameValid(username)) {
throw new IllegalArgumentException(getInvalidUsernameMessage()); throw new IllegalArgumentException(getInvalidUsernameMessage());
} }
@@ -144,7 +150,7 @@ public class UserService implements UserServiceInterface {
user.setUsername(username); user.setUsername(username);
user.setEnabled(true); user.setEnabled(true);
user.setFirstLogin(false); user.setFirstLogin(false);
user.addAuthority(new Authority(Role.USER.getRoleId(), user)); user.addAuthority(new Authority(role, user));
user.setAuthenticationType(authenticationType); user.setAuthenticationType(authenticationType);
userRepository.save(user); userRepository.save(user);
databaseBackupHelper.exportDatabase(); databaseBackupHelper.exportDatabase();
@@ -186,7 +192,7 @@ public class UserService implements UserServiceInterface {
} }
public void deleteUser(String username) { public void deleteUser(String username) {
Optional<User> userOpt = userRepository.findByUsernameIgnoreCase(username); Optional<User> userOpt = findByUsernameIgnoreCase(username);
if (userOpt.isPresent()) { if (userOpt.isPresent()) {
for (Authority authority : userOpt.get().getAuthorities()) { for (Authority authority : userOpt.get().getAuthorities()) {
if (authority.getAuthority().equals(Role.INTERNAL_API_USER.getRoleId())) { if (authority.getAuthority().equals(Role.INTERNAL_API_USER.getRoleId())) {
@@ -195,21 +201,20 @@ public class UserService implements UserServiceInterface {
} }
userRepository.delete(userOpt.get()); userRepository.delete(userOpt.get());
} }
invalidateUserSessions(username);
} }
public boolean usernameExists(String username) { public boolean usernameExists(String username) {
return userRepository.findByUsername(username).isPresent(); return findByUsername(username).isPresent();
} }
public boolean usernameExistsIgnoreCase(String username) { public boolean usernameExistsIgnoreCase(String username) {
return userRepository.findByUsernameIgnoreCase(username).isPresent(); return findByUsernameIgnoreCase(username).isPresent();
} }
public boolean hasUsers() { public boolean hasUsers() {
long userCount = userRepository.count(); long userCount = userRepository.count();
if (userRepository if (findByUsernameIgnoreCase(Role.INTERNAL_API_USER.getRoleId()).isPresent()) {
.findByUsernameIgnoreCase(Role.INTERNAL_API_USER.getRoleId())
.isPresent()) {
userCount -= 1; userCount -= 1;
} }
return userCount > 0; return userCount > 0;
@@ -217,7 +222,7 @@ public class UserService implements UserServiceInterface {
public void updateUserSettings(String username, Map<String, String> updates) public void updateUserSettings(String username, Map<String, String> updates)
throws IOException { throws IOException {
Optional<User> userOpt = userRepository.findByUsernameIgnoreCase(username); Optional<User> userOpt = findByUsernameIgnoreCase(username);
if (userOpt.isPresent()) { if (userOpt.isPresent()) {
User user = userOpt.get(); User user = userOpt.get();
Map<String, String> settingsMap = user.getSettings(); Map<String, String> settingsMap = user.getSettings();
@@ -268,10 +273,17 @@ public class UserService implements UserServiceInterface {
databaseBackupHelper.exportDatabase(); databaseBackupHelper.exportDatabase();
} }
public void changeRole(User user, String newRole) { public void changeRole(User user, String newRole) throws IOException {
Authority userAuthority = this.findRole(user); Authority userAuthority = this.findRole(user);
userAuthority.setAuthority(newRole); userAuthority.setAuthority(newRole);
authorityRepository.save(userAuthority); authorityRepository.save(userAuthority);
databaseBackupHelper.exportDatabase();
}
public void changeUserEnabled(User user, Boolean enbeled) throws IOException {
user.setEnabled(enbeled);
userRepository.save(user);
databaseBackupHelper.exportDatabase();
} }
public boolean isPasswordCorrect(User user, String currentPassword) { public boolean isPasswordCorrect(User user, String currentPassword) {
@@ -295,14 +307,40 @@ public class UserService implements UserServiceInterface {
} }
public boolean hasPassword(String username) { public boolean hasPassword(String username) {
Optional<User> user = userRepository.findByUsernameIgnoreCase(username); Optional<User> user = findByUsernameIgnoreCase(username);
return user.isPresent() && user.get().hasPassword(); return user.isPresent() && user.get().hasPassword();
} }
public boolean isAuthenticationTypeByUsername( public boolean isAuthenticationTypeByUsername(
String username, AuthenticationType authenticationType) { String username, AuthenticationType authenticationType) {
Optional<User> user = userRepository.findByUsernameIgnoreCase(username); Optional<User> user = findByUsernameIgnoreCase(username);
return user.isPresent() return user.isPresent()
&& authenticationType.name().equalsIgnoreCase(user.get().getAuthenticationType()); && authenticationType.name().equalsIgnoreCase(user.get().getAuthenticationType());
} }
public boolean isUserDisabled(String username) {
Optional<User> userOpt = findByUsernameIgnoreCase(username);
return userOpt.map(user -> !user.isEnabled()).orElse(false);
}
public void invalidateUserSessions(String username) {
String usernameP = "";
for (Object principal : sessionRegistry.getAllPrincipals()) {
for (SessionInformation sessionsInformation :
sessionRegistry.getAllSessions(principal, false)) {
if (principal instanceof UserDetails) {
UserDetails userDetails = (UserDetails) principal;
usernameP = userDetails.getUsername();
} else if (principal instanceof OAuth2User) {
OAuth2User oAuth2User = (OAuth2User) principal;
usernameP = oAuth2User.getName();
} else if (principal instanceof String) {
usernameP = (String) principal;
}
if (usernameP.equalsIgnoreCase(username)) {
sessionRegistry.expireSession(sessionsInformation.getSessionId());
}
}
}
}
} }

View File

@@ -8,6 +8,7 @@ import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.BasicFileAttributes;
import java.sql.Connection; import java.sql.Connection;
import java.sql.DriverManager; import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.sql.Statement; import java.sql.Statement;
@@ -131,11 +132,12 @@ public class DatabaseBackupHelper implements DatabaseBackupInterface {
DateTimeFormatter myFormatObj = DateTimeFormatter.ofPattern("yyyyMMddHHmm"); DateTimeFormatter myFormatObj = DateTimeFormatter.ofPattern("yyyyMMddHHmm");
Path insertOutputFilePath = Path insertOutputFilePath =
this.getBackupFilePath("backup_" + dateNow.format(myFormatObj) + ".sql"); this.getBackupFilePath("backup_" + dateNow.format(myFormatObj) + ".sql");
String query = "SCRIPT SIMPLE COLUMNS DROP to '" + insertOutputFilePath.toString() + "';"; String query = "SCRIPT SIMPLE COLUMNS DROP to ?;";
try (Connection conn = DriverManager.getConnection(url, "sa", ""); try (Connection conn = DriverManager.getConnection(url, "sa", "");
Statement stmt = conn.createStatement()) { PreparedStatement stmt = conn.prepareStatement(query)) {
stmt.execute(query); stmt.setString(1, insertOutputFilePath.toString());
stmt.execute();
log.info("Database export completed: {}", insertOutputFilePath); log.info("Database export completed: {}", insertOutputFilePath);
} catch (SQLException e) { } catch (SQLException e) {
log.error("Error during database export: {}", e.getMessage(), e); log.error("Error during database export: {}", e.getMessage(), e);
@@ -177,11 +179,12 @@ public class DatabaseBackupHelper implements DatabaseBackupInterface {
} }
private boolean executeDatabaseScript(Path scriptPath) { private boolean executeDatabaseScript(Path scriptPath) {
try (Connection conn = DriverManager.getConnection(url, "sa", ""); String query = "RUNSCRIPT from ?;";
Statement stmt = conn.createStatement()) {
String query = "RUNSCRIPT from '" + scriptPath.toString() + "';"; try (Connection conn = DriverManager.getConnection(url, "sa", "");
stmt.execute(query); PreparedStatement stmt = conn.prepareStatement(query)) {
stmt.setString(1, scriptPath.toString());
stmt.execute();
log.info("Database import completed: {}", scriptPath); log.info("Database import completed: {}", scriptPath);
return true; return true;
} catch (SQLException e) { } catch (SQLException e) {

View File

@@ -2,8 +2,8 @@ package stirling.software.SPDF.config.security.oauth2;
import java.io.IOException; import java.io.IOException;
import org.slf4j.Logger; import org.springframework.security.authentication.BadCredentialsException;
import org.slf4j.LoggerFactory; import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.LockedException; import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
@@ -13,19 +13,34 @@ import org.springframework.security.web.authentication.SimpleUrlAuthenticationFa
import jakarta.servlet.ServletException; import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class CustomOAuth2AuthenticationFailureHandler public class CustomOAuth2AuthenticationFailureHandler
extends SimpleUrlAuthenticationFailureHandler { extends SimpleUrlAuthenticationFailureHandler {
private static final Logger logger =
LoggerFactory.getLogger(CustomOAuth2AuthenticationFailureHandler.class);
@Override @Override
public void onAuthenticationFailure( public void onAuthenticationFailure(
HttpServletRequest request, HttpServletRequest request,
HttpServletResponse response, HttpServletResponse response,
AuthenticationException exception) AuthenticationException exception)
throws IOException, ServletException { throws IOException, ServletException {
if (exception instanceof BadCredentialsException) {
log.error("BadCredentialsException", exception);
getRedirectStrategy().sendRedirect(request, response, "/login?error=badcredentials");
return;
}
if (exception instanceof DisabledException) {
log.error("User is deactivated: ", exception);
getRedirectStrategy().sendRedirect(request, response, "/logout?userIsDisabled=true");
return;
}
if (exception instanceof LockedException) {
log.error("Account locked: ", exception);
getRedirectStrategy().sendRedirect(request, response, "/logout?error=locked");
return;
}
if (exception instanceof OAuth2AuthenticationException) { if (exception instanceof OAuth2AuthenticationException) {
OAuth2Error error = ((OAuth2AuthenticationException) exception).getError(); OAuth2Error error = ((OAuth2AuthenticationException) exception).getError();
@@ -34,17 +49,13 @@ public class CustomOAuth2AuthenticationFailureHandler
if (error.getErrorCode().equals("Password must not be null")) { if (error.getErrorCode().equals("Password must not be null")) {
errorCode = "userAlreadyExistsWeb"; errorCode = "userAlreadyExistsWeb";
} }
logger.error("OAuth2 Authentication error: " + errorCode); log.error("OAuth2 Authentication error: " + errorCode);
log.error("OAuth2AuthenticationException", exception);
getRedirectStrategy() getRedirectStrategy()
.sendRedirect(request, response, "/logout?erroroauth=" + errorCode); .sendRedirect(request, response, "/logout?erroroauth=" + errorCode);
return; return;
} else if (exception instanceof LockedException) { }
logger.error("Account locked: ", exception); log.error("Unhandled authentication exception", exception);
getRedirectStrategy().sendRedirect(request, response, "/logout?error=locked");
return;
} else {
logger.error("Unhandled authentication exception", exception);
super.onAuthenticationFailure(request, response, exception); super.onAuthenticationFailure(request, response, exception);
} }
} }
}

View File

@@ -2,10 +2,9 @@ package stirling.software.SPDF.config.security.oauth2;
import java.io.IOException; import java.io.IOException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.LockedException; import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler; import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.security.web.savedrequest.SavedRequest; import org.springframework.security.web.savedrequest.SavedRequest;
@@ -26,9 +25,6 @@ public class CustomOAuth2AuthenticationSuccessHandler
private LoginAttemptService loginAttemptService; private LoginAttemptService loginAttemptService;
private static final Logger logger =
LoggerFactory.getLogger(CustomOAuth2AuthenticationSuccessHandler.class);
private ApplicationProperties applicationProperties; private ApplicationProperties applicationProperties;
private UserService userService; private UserService userService;
@@ -46,6 +42,17 @@ public class CustomOAuth2AuthenticationSuccessHandler
HttpServletRequest request, HttpServletResponse response, Authentication authentication) HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws ServletException, IOException { throws ServletException, IOException {
Object principal = authentication.getPrincipal();
String username = "";
if (principal instanceof OAuth2User) {
OAuth2User oauthUser = (OAuth2User) principal;
username = oauthUser.getName();
} else if (principal instanceof UserDetails) {
UserDetails oauthUser = (UserDetails) principal;
username = oauthUser.getUsername();
}
// Get the saved request // Get the saved request
HttpSession session = request.getSession(false); HttpSession session = request.getSession(false);
String contextPath = request.getContextPath(); String contextPath = request.getContextPath();
@@ -59,11 +66,8 @@ public class CustomOAuth2AuthenticationSuccessHandler
// Redirect to the original destination // Redirect to the original destination
super.onAuthenticationSuccess(request, response, authentication); super.onAuthenticationSuccess(request, response, authentication);
} else { } else {
OAuth2User oauthUser = (OAuth2User) authentication.getPrincipal();
OAUTH2 oAuth = applicationProperties.getSecurity().getOAUTH2(); OAUTH2 oAuth = applicationProperties.getSecurity().getOAUTH2();
String username = oauthUser.getName();
if (loginAttemptService.isBlocked(username)) { if (loginAttemptService.isBlocked(username)) {
if (session != null) { if (session != null) {
session.removeAttribute("SPRING_SECURITY_SAVED_REQUEST"); session.removeAttribute("SPRING_SECURITY_SAVED_REQUEST");
@@ -78,9 +82,16 @@ public class CustomOAuth2AuthenticationSuccessHandler
&& oAuth.getAutoCreateUser()) { && oAuth.getAutoCreateUser()) {
response.sendRedirect(contextPath + "/logout?oauth2AuthenticationErrorWeb=true"); response.sendRedirect(contextPath + "/logout?oauth2AuthenticationErrorWeb=true");
return; return;
} else { }
try { try {
if (oAuth.getBlockRegistration()
&& !userService.usernameExistsIgnoreCase(username)) {
response.sendRedirect(contextPath + "/logout?oauth2_admin_blocked_user=true");
return;
}
if (principal instanceof OAuth2User) {
userService.processOAuth2PostLogin(username, oAuth.getAutoCreateUser()); userService.processOAuth2PostLogin(username, oAuth.getAutoCreateUser());
}
response.sendRedirect(contextPath + "/"); response.sendRedirect(contextPath + "/");
return; return;
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
@@ -90,4 +101,3 @@ public class CustomOAuth2AuthenticationSuccessHandler
} }
} }
} }
}

View File

@@ -2,34 +2,26 @@ package stirling.software.SPDF.config.security.oauth2;
import java.io.IOException; import java.io.IOException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler; import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
import jakarta.servlet.ServletException; import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession; import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2; import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
import stirling.software.SPDF.model.Provider; import stirling.software.SPDF.model.Provider;
import stirling.software.SPDF.model.provider.UnsupportedProviderException; import stirling.software.SPDF.model.provider.UnsupportedProviderException;
import stirling.software.SPDF.utils.UrlUtils; import stirling.software.SPDF.utils.UrlUtils;
@Slf4j
public class CustomOAuth2LogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler { public class CustomOAuth2LogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
private static final Logger logger =
LoggerFactory.getLogger(CustomOAuth2LogoutSuccessHandler.class);
private final SessionRegistry sessionRegistry;
private final ApplicationProperties applicationProperties; private final ApplicationProperties applicationProperties;
public CustomOAuth2LogoutSuccessHandler( public CustomOAuth2LogoutSuccessHandler(ApplicationProperties applicationProperties) {
ApplicationProperties applicationProperties, SessionRegistry sessionRegistry) {
this.sessionRegistry = sessionRegistry;
this.applicationProperties = applicationProperties; this.applicationProperties = applicationProperties;
} }
@@ -42,6 +34,15 @@ public class CustomOAuth2LogoutSuccessHandler extends SimpleUrlLogoutSuccessHand
String issuer = null; String issuer = null;
String clientId = null; String clientId = null;
if (authentication == null) {
if (request.getParameter("userIsDisabled") != null) {
response.sendRedirect(
request.getContextPath() + "/login?erroroauth=userIsDisabled");
} else {
super.onLogoutSuccess(request, response, authentication);
}
return;
}
OAUTH2 oauth = applicationProperties.getSecurity().getOAUTH2(); OAUTH2 oauth = applicationProperties.getSecurity().getOAUTH2();
if (authentication instanceof OAuth2AuthenticationToken) { if (authentication instanceof OAuth2AuthenticationToken) {
@@ -53,9 +54,8 @@ public class CustomOAuth2LogoutSuccessHandler extends SimpleUrlLogoutSuccessHand
issuer = provider.getIssuer(); issuer = provider.getIssuer();
clientId = provider.getClientId(); clientId = provider.getClientId();
} catch (UnsupportedProviderException e) { } catch (UnsupportedProviderException e) {
logger.error(e.getMessage()); log.error(e.getMessage());
} }
} else { } else {
registrationId = oauth.getProvider() != null ? oauth.getProvider() : ""; registrationId = oauth.getProvider() != null ? oauth.getProvider() : "";
issuer = oauth.getIssuer(); issuer = oauth.getIssuer();
@@ -70,18 +70,16 @@ public class CustomOAuth2LogoutSuccessHandler extends SimpleUrlLogoutSuccessHand
param = "erroroauth=" + sanitizeInput(errorMessage); param = "erroroauth=" + sanitizeInput(errorMessage);
} else if (request.getParameter("oauth2AutoCreateDisabled") != null) { } else if (request.getParameter("oauth2AutoCreateDisabled") != null) {
param = "error=oauth2AutoCreateDisabled"; param = "error=oauth2AutoCreateDisabled";
} else if (request.getParameter("oauth2_admin_blocked_user") != null) {
param = "erroroauth=oauth2_admin_blocked_user";
} else if (request.getParameter("userIsDisabled") != null) {
param = "erroroauth=userIsDisabled";
} else if (request.getParameter("badcredentials") != null) {
param = "error=badcredentials";
} }
String redirect_url = UrlUtils.getOrigin(request) + "/login?" + param; String redirect_url = UrlUtils.getOrigin(request) + "/login?" + param;
HttpSession session = request.getSession(false);
if (session != null) {
String sessionId = session.getId();
sessionRegistry.removeSessionInformation(sessionId);
session.invalidate();
logger.info("Session invalidated: " + sessionId);
}
switch (registrationId.toLowerCase()) { switch (registrationId.toLowerCase()) {
case "keycloak": case "keycloak":
// Add Keycloak specific logout URL if needed // Add Keycloak specific logout URL if needed
@@ -92,13 +90,13 @@ public class CustomOAuth2LogoutSuccessHandler extends SimpleUrlLogoutSuccessHand
+ clientId + clientId
+ "&post_logout_redirect_uri=" + "&post_logout_redirect_uri="
+ response.encodeRedirectURL(redirect_url); + response.encodeRedirectURL(redirect_url);
logger.info("Redirecting to Keycloak logout URL: " + logoutUrl); log.info("Redirecting to Keycloak logout URL: " + logoutUrl);
response.sendRedirect(logoutUrl); response.sendRedirect(logoutUrl);
break; break;
case "github": case "github":
// Add GitHub specific logout URL if needed // Add GitHub specific logout URL if needed
String githubLogoutUrl = "https://github.com/logout"; String githubLogoutUrl = "https://github.com/logout";
logger.info("Redirecting to GitHub logout URL: " + githubLogoutUrl); log.info("Redirecting to GitHub logout URL: " + githubLogoutUrl);
response.sendRedirect(githubLogoutUrl); response.sendRedirect(githubLogoutUrl);
break; break;
case "google": case "google":
@@ -106,13 +104,14 @@ public class CustomOAuth2LogoutSuccessHandler extends SimpleUrlLogoutSuccessHand
// String googleLogoutUrl = // String googleLogoutUrl =
// "https://accounts.google.com/Logout?continue=https://appengine.google.com/_ah/logout?continue=" // "https://accounts.google.com/Logout?continue=https://appengine.google.com/_ah/logout?continue="
// + response.encodeRedirectURL(redirect_url); // + response.encodeRedirectURL(redirect_url);
// logger.info("Redirecting to Google logout URL: " + googleLogoutUrl); log.info("Google does not have a specific logout URL");
// log.info("Redirecting to Google logout URL: " + googleLogoutUrl);
// response.sendRedirect(googleLogoutUrl); // response.sendRedirect(googleLogoutUrl);
// break; // break;
default: default:
String redirectUrl = request.getContextPath() + "/login?" + param; String defaultRedirectUrl = request.getContextPath() + "/login?" + param;
logger.info("Redirecting to default logout URL: " + redirectUrl); log.info("Redirecting to default logout URL: " + defaultRedirectUrl);
response.sendRedirect(redirectUrl); response.sendRedirect(defaultRedirectUrl);
break; break;
} }
} }

View File

@@ -0,0 +1,26 @@
package stirling.software.SPDF.config.security.session;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import jakarta.servlet.http.HttpSessionEvent;
import jakarta.servlet.http.HttpSessionListener;
import lombok.extern.slf4j.Slf4j;
@Component
@Slf4j
public class CustomHttpSessionListener implements HttpSessionListener {
@Autowired private SessionPersistentRegistry sessionPersistentRegistry;
@Override
public void sessionCreated(HttpSessionEvent se) {
log.info("Session created: " + se.getSession().getId());
}
@Override
public void sessionDestroyed(HttpSessionEvent se) {
log.info("Session destroyed: " + se.getSession().getId());
sessionPersistentRegistry.expireSession(se.getSession().getId());
}
}

View File

@@ -0,0 +1,183 @@
package stirling.software.SPDF.config.security.session;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.session.SessionInformation;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Component;
import jakarta.transaction.Transactional;
import stirling.software.SPDF.model.SessionEntity;
@Component
public class SessionPersistentRegistry implements SessionRegistry {
private final SessionRepository sessionRepository;
@Value("${server.servlet.session.timeout:30m}")
private Duration defaultMaxInactiveInterval;
public SessionPersistentRegistry(SessionRepository sessionRepository) {
this.sessionRepository = sessionRepository;
}
@Override
public List<Object> getAllPrincipals() {
List<SessionEntity> sessions = sessionRepository.findAll();
List<Object> principals = new ArrayList<>();
for (SessionEntity session : sessions) {
principals.add(session.getPrincipalName());
}
return principals;
}
@Override
public List<SessionInformation> getAllSessions(
Object principal, boolean includeExpiredSessions) {
List<SessionInformation> sessionInformations = new ArrayList<>();
String principalName = null;
if (principal instanceof UserDetails) {
principalName = ((UserDetails) principal).getUsername();
} else if (principal instanceof OAuth2User) {
principalName = ((OAuth2User) principal).getName();
} else if (principal instanceof String) {
principalName = (String) principal;
}
if (principalName != null) {
List<SessionEntity> sessionEntities =
sessionRepository.findByPrincipalName(principalName);
for (SessionEntity sessionEntity : sessionEntities) {
if (includeExpiredSessions || !sessionEntity.isExpired()) {
sessionInformations.add(
new SessionInformation(
sessionEntity.getPrincipalName(),
sessionEntity.getSessionId(),
sessionEntity.getLastRequest()));
}
}
}
return sessionInformations;
}
@Override
@Transactional
public void registerNewSession(String sessionId, Object principal) {
String principalName = null;
if (principal instanceof UserDetails) {
principalName = ((UserDetails) principal).getUsername();
} else if (principal instanceof OAuth2User) {
principalName = ((OAuth2User) principal).getName();
} else if (principal instanceof String) {
principalName = (String) principal;
}
if (principalName != null) {
SessionEntity sessionEntity = new SessionEntity();
sessionEntity.setSessionId(sessionId);
sessionEntity.setPrincipalName(principalName);
sessionEntity.setLastRequest(new Date()); // Set lastRequest to the current date
sessionEntity.setExpired(false);
sessionRepository.save(sessionEntity);
}
}
@Override
@Transactional
public void removeSessionInformation(String sessionId) {
sessionRepository.deleteById(sessionId);
}
@Override
@Transactional
public void refreshLastRequest(String sessionId) {
Optional<SessionEntity> sessionEntityOpt = sessionRepository.findById(sessionId);
if (sessionEntityOpt.isPresent()) {
SessionEntity sessionEntity = sessionEntityOpt.get();
sessionEntity.setLastRequest(new Date()); // Update lastRequest to the current date
sessionRepository.save(sessionEntity);
}
}
@Override
public SessionInformation getSessionInformation(String sessionId) {
Optional<SessionEntity> sessionEntityOpt = sessionRepository.findById(sessionId);
if (sessionEntityOpt.isPresent()) {
SessionEntity sessionEntity = sessionEntityOpt.get();
return new SessionInformation(
sessionEntity.getPrincipalName(),
sessionEntity.getSessionId(),
sessionEntity.getLastRequest());
}
return null;
}
// Retrieve all non-expired sessions
public List<SessionEntity> getAllSessionsNotExpired() {
return sessionRepository.findByExpired(false);
}
// Retrieve all sessions
public List<SessionEntity> getAllSessions() {
return sessionRepository.findAll();
}
// Mark a session as expired
public void expireSession(String sessionId) {
Optional<SessionEntity> sessionEntityOpt = sessionRepository.findById(sessionId);
if (sessionEntityOpt.isPresent()) {
SessionEntity sessionEntity = sessionEntityOpt.get();
sessionEntity.setExpired(true); // Set expired to true
sessionRepository.save(sessionEntity);
}
}
// Get the maximum inactive interval for sessions
public int getMaxInactiveInterval() {
return (int) defaultMaxInactiveInterval.getSeconds();
}
// Retrieve a session entity by session ID
public SessionEntity getSessionEntity(String sessionId) {
return sessionRepository.findBySessionId(sessionId);
}
// Update session details by principal name
public void updateSessionByPrincipalName(
String principalName, boolean expired, Date lastRequest) {
sessionRepository.saveByPrincipalName(expired, lastRequest, principalName);
}
// Find the latest session for a given principal name
public Optional<SessionEntity> findLatestSession(String principalName) {
List<SessionEntity> allSessions = sessionRepository.findByPrincipalName(principalName);
if (allSessions.isEmpty()) {
return Optional.empty();
}
// Sort sessions by lastRequest in descending order
Collections.sort(
allSessions,
new Comparator<SessionEntity>() {
@Override
public int compare(SessionEntity s1, SessionEntity s2) {
// Sort by lastRequest in descending order
return s2.getLastRequest().compareTo(s1.getLastRequest());
}
});
// The first session in the list is the latest session for the given principal name
return Optional.of(allSessions.get(0));
}
}

View File

@@ -0,0 +1,20 @@
package stirling.software.SPDF.config.security.session;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.session.SessionRegistryImpl;
@Configuration
public class SessionRegistryConfig {
@Bean
public SessionRegistryImpl sessionRegistry() {
return new SessionRegistryImpl();
}
@Bean
public SessionPersistentRegistry sessionPersistentRegistry(
SessionRepository sessionRepository) {
return new SessionPersistentRegistry(sessionRepository);
}
}

View File

@@ -0,0 +1,31 @@
package stirling.software.SPDF.config.security.session;
import java.util.Date;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import jakarta.transaction.Transactional;
import stirling.software.SPDF.model.SessionEntity;
@Repository
public interface SessionRepository extends JpaRepository<SessionEntity, String> {
List<SessionEntity> findByPrincipalName(String principalName);
List<SessionEntity> findByExpired(boolean expired);
SessionEntity findBySessionId(String sessionId);
@Modifying
@Transactional
@Query(
"UPDATE SessionEntity s SET s.expired = :expired, s.lastRequest = :lastRequest WHERE s.principalName = :principalName")
void saveByPrincipalName(
@Param("expired") boolean expired,
@Param("lastRequest") Date lastRequest,
@Param("principalName") String principalName);
}

View File

@@ -0,0 +1,35 @@
package stirling.software.SPDF.config.security.session;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Date;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.security.core.session.SessionInformation;
import org.springframework.stereotype.Component;
@Component
public class SessionScheduled {
@Autowired private SessionPersistentRegistry sessionPersistentRegistry;
@Scheduled(cron = "0 0/5 * * * ?")
public void expireSessions() {
Instant now = Instant.now();
for (Object principal : sessionPersistentRegistry.getAllPrincipals()) {
List<SessionInformation> sessionInformations =
sessionPersistentRegistry.getAllSessions(principal, false);
for (SessionInformation sessionInformation : sessionInformations) {
Date lastRequest = sessionInformation.getLastRequest();
int maxInactiveInterval = sessionPersistentRegistry.getMaxInactiveInterval();
Instant expirationTime =
lastRequest.toInstant().plus(maxInactiveInterval, ChronoUnit.SECONDS);
if (now.isAfter(expirationTime)) {
sessionPersistentRegistry.expireSession(sessionInformation.getSessionId());
}
}
}
}
}

View File

@@ -0,0 +1,82 @@
package stirling.software.SPDF.controller.api;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation;
import stirling.software.SPDF.model.api.PDFFile;
import stirling.software.SPDF.service.PdfImageRemovalService;
import stirling.software.SPDF.utils.WebResponseUtils;
/**
* Controller class for handling PDF image removal requests. Provides an endpoint to remove images
* from a PDF file to reduce its size.
*/
@RestController
@RequestMapping("/api/v1/general")
public class PdfImageRemovalController {
// Service for removing images from PDFs
@Autowired private PdfImageRemovalService pdfImageRemovalService;
/**
* Constructor for dependency injection of PdfImageRemovalService.
*
* @param pdfImageRemovalService The service used for removing images from PDFs.
*/
public PdfImageRemovalController(PdfImageRemovalService pdfImageRemovalService) {
this.pdfImageRemovalService = pdfImageRemovalService;
}
/**
* Endpoint to remove images from a PDF file.
*
* <p>This method processes the uploaded PDF file, removes all images, and returns the modified
* PDF file with a new name indicating that images were removed.
*
* @param file The PDF file with images to be removed.
* @return ResponseEntity containing the modified PDF file as byte array with appropriate
* content type and filename.
* @throws IOException If an error occurs while processing the PDF file.
*/
@PostMapping(consumes = "multipart/form-data", value = "/remove-image-pdf")
@Operation(
summary = "Remove images from file to reduce the file size.",
description =
"This endpoint remove images from file to reduce the file size.Input:PDF Output:PDF Type:MISO")
public ResponseEntity<byte[]> removeImages(@ModelAttribute PDFFile file) throws IOException {
MultipartFile pdf = file.getFileInput();
// Convert the MultipartFile to a byte array
byte[] pdfBytes = pdf.getBytes();
// Load the PDF document from the byte array
PDDocument document = Loader.loadPDF(pdfBytes);
// Remove images from the PDF document using the service
PDDocument modifiedDocument = pdfImageRemovalService.removeImagesFromPdf(document);
// Create a ByteArrayOutputStream to hold the modified PDF data
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
// Save the modified PDF document to the output stream
modifiedDocument.save(outputStream);
modifiedDocument.close();
// Generate a new filename for the modified PDF
String mergedFileName =
pdf.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_removed_images.pdf";
// Convert the byte array to a web response and return it
return WebResponseUtils.bytesToWebResponse(outputStream.toByteArray(), mergedFileName);
}
}

View File

@@ -3,6 +3,7 @@ package stirling.software.SPDF.controller.api;
import java.io.IOException; import java.io.IOException;
import java.security.Principal; import java.security.Principal;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
@@ -12,8 +13,8 @@ import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.session.SessionInformation; import org.springframework.security.core.session.SessionInformation;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler; import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.ui.Model; import org.springframework.ui.Model;
@@ -30,6 +31,8 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import stirling.software.SPDF.config.security.UserService; import stirling.software.SPDF.config.security.UserService;
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
import stirling.software.SPDF.model.AuthenticationType;
import stirling.software.SPDF.model.Role; import stirling.software.SPDF.model.Role;
import stirling.software.SPDF.model.User; import stirling.software.SPDF.model.User;
import stirling.software.SPDF.model.api.user.UsernameAndPass; import stirling.software.SPDF.model.api.user.UsernameAndPass;
@@ -41,6 +44,8 @@ public class UserController {
@Autowired private UserService userService; @Autowired private UserService userService;
@Autowired SessionPersistentRegistry sessionRegistry;
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')") @PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
@PostMapping("/register") @PostMapping("/register")
public String register(@ModelAttribute UsernameAndPass requestModel, Model model) public String register(@ModelAttribute UsernameAndPass requestModel, Model model)
@@ -203,9 +208,10 @@ public class UserController {
@PreAuthorize("hasRole('ROLE_ADMIN')") @PreAuthorize("hasRole('ROLE_ADMIN')")
@PostMapping("/admin/saveUser") @PostMapping("/admin/saveUser")
public RedirectView saveUser( public RedirectView saveUser(
@RequestParam(name = "username") String username, @RequestParam String username,
@RequestParam(name = "password") String password, @RequestParam(name = "password", required = false) String password,
@RequestParam(name = "role") String role, @RequestParam(name = "role") String role,
@RequestParam(name = "authType") String authType,
@RequestParam(name = "forceChange", required = false, defaultValue = "false") @RequestParam(name = "forceChange", required = false, defaultValue = "false")
boolean forceChange) boolean forceChange)
throws IllegalArgumentException, IOException { throws IllegalArgumentException, IOException {
@@ -237,7 +243,15 @@ public class UserController {
return new RedirectView("/addUsers?messageType=invalidRole", true); return new RedirectView("/addUsers?messageType=invalidRole", true);
} }
if (authType.equalsIgnoreCase(AuthenticationType.OAUTH2.toString())) {
userService.saveUser(username, AuthenticationType.OAUTH2, role);
} else {
if (password.isBlank()) {
return new RedirectView("/addUsers?messageType=invalidPassword", true);
}
userService.saveUser(username, password, role, forceChange); userService.saveUser(username, password, role, forceChange);
}
return new RedirectView( return new RedirectView(
"/addUsers", true); // Redirect to account page after adding the user "/addUsers", true); // Redirect to account page after adding the user
} }
@@ -247,7 +261,8 @@ public class UserController {
public RedirectView changeRole( public RedirectView changeRole(
@RequestParam(name = "username") String username, @RequestParam(name = "username") String username,
@RequestParam(name = "role") String role, @RequestParam(name = "role") String role,
Authentication authentication) { Authentication authentication)
throws IOException {
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username); Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
@@ -278,6 +293,60 @@ public class UserController {
User user = userOpt.get(); User user = userOpt.get();
userService.changeRole(user, role); userService.changeRole(user, role);
return new RedirectView(
"/addUsers", true); // Redirect to account page after adding the user
}
@PreAuthorize("hasRole('ROLE_ADMIN')")
@PostMapping("/admin/changeUserEnabled/{username}")
public RedirectView changeUserEnabled(
@PathVariable("username") String username,
@RequestParam("enabled") boolean enabled,
Authentication authentication)
throws IOException {
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
if (!userOpt.isPresent()) {
return new RedirectView("/addUsers?messageType=userNotFound", true);
}
if (!userService.usernameExistsIgnoreCase(username)) {
return new RedirectView("/addUsers?messageType=userNotFound", true);
}
// Get the currently authenticated username
String currentUsername = authentication.getName();
// Check if the provided username matches the current session's username
if (currentUsername.equalsIgnoreCase(username)) {
return new RedirectView("/addUsers?messageType=disabledCurrentUser", true);
}
User user = userOpt.get();
userService.changeUserEnabled(user, enabled);
if (!enabled) {
// Invalidate all sessions if the user is being disabled
List<Object> principals = sessionRegistry.getAllPrincipals();
String userNameP = "";
for (Object principal : principals) {
List<SessionInformation> sessionsInformations =
sessionRegistry.getAllSessions(principal, false);
if (principal instanceof UserDetails) {
userNameP = ((UserDetails) principal).getUsername();
} else if (principal instanceof OAuth2User) {
userNameP = ((OAuth2User) principal).getName();
} else if (principal instanceof String) {
userNameP = (String) principal;
}
if (userNameP.equalsIgnoreCase(username)) {
for (SessionInformation sessionsInformation : sessionsInformations) {
sessionRegistry.expireSession(sessionsInformation.getSessionId());
}
}
}
}
return new RedirectView( return new RedirectView(
"/addUsers", true); // Redirect to account page after adding the user "/addUsers", true); // Redirect to account page after adding the user
} }
@@ -285,7 +354,7 @@ public class UserController {
@PreAuthorize("hasRole('ROLE_ADMIN')") @PreAuthorize("hasRole('ROLE_ADMIN')")
@PostMapping("/admin/deleteUser/{username}") @PostMapping("/admin/deleteUser/{username}")
public RedirectView deleteUser( public RedirectView deleteUser(
@PathVariable(name = "username") String username, Authentication authentication) { @PathVariable("username") String username, Authentication authentication) {
if (!userService.usernameExistsIgnoreCase(username)) { if (!userService.usernameExistsIgnoreCase(username)) {
return new RedirectView("/addUsers?messageType=deleteUsernameExists", true); return new RedirectView("/addUsers?messageType=deleteUsernameExists", true);
@@ -298,27 +367,18 @@ public class UserController {
if (currentUsername.equalsIgnoreCase(username)) { if (currentUsername.equalsIgnoreCase(username)) {
return new RedirectView("/addUsers?messageType=deleteCurrentUser", true); return new RedirectView("/addUsers?messageType=deleteCurrentUser", true);
} }
invalidateUserSessions(username);
// Invalidate all sessions before deleting the user
List<SessionInformation> sessionsInformations =
sessionRegistry.getAllSessions(authentication.getPrincipal(), false);
for (SessionInformation sessionsInformation : sessionsInformations) {
sessionRegistry.expireSession(sessionsInformation.getSessionId());
sessionRegistry.removeSessionInformation(sessionsInformation.getSessionId());
}
userService.deleteUser(username); userService.deleteUser(username);
return new RedirectView("/addUsers", true); return new RedirectView("/addUsers", true);
} }
@Autowired private SessionRegistry sessionRegistry;
private void invalidateUserSessions(String username) {
for (Object principal : sessionRegistry.getAllPrincipals()) {
if (principal instanceof UserDetails) {
UserDetails userDetails = (UserDetails) principal;
if (userDetails.getUsername().equals(username)) {
for (SessionInformation session :
sessionRegistry.getAllSessions(principal, false)) {
session.expireNow();
}
}
}
}
}
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')") @PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
@PostMapping("/get-api-key") @PostMapping("/get-api-key")
public ResponseEntity<String> getApiKey(Principal principal) { public ResponseEntity<String> getApiKey(Principal principal) {

View File

@@ -1,11 +1,23 @@
package stirling.software.SPDF.controller.api.converters; package stirling.software.SPDF.controller.api.converters;
import java.io.ByteArrayOutputStream;
import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.net.URLConnection; import java.net.URLConnection;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import org.apache.commons.io.FileUtils;
import org.apache.pdfbox.rendering.ImageType; import org.apache.pdfbox.rendering.ImageType;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.ModelAttribute;
@@ -21,6 +33,8 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.converters.ConvertToImageRequest; import stirling.software.SPDF.model.api.converters.ConvertToImageRequest;
import stirling.software.SPDF.model.api.converters.ConvertToPdfRequest; import stirling.software.SPDF.model.api.converters.ConvertToPdfRequest;
import stirling.software.SPDF.utils.PdfUtils; import stirling.software.SPDF.utils.PdfUtils;
import stirling.software.SPDF.utils.ProcessExecutor;
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@@ -60,15 +74,92 @@ public class ConvertImgPDFController {
result = result =
PdfUtils.convertFromPdf( PdfUtils.convertFromPdf(
pdfBytes, pdfBytes,
imageFormat.toUpperCase(), imageFormat.equalsIgnoreCase("webp") ? "png" : imageFormat.toUpperCase(),
colorTypeResult, colorTypeResult,
singleImage, singleImage,
Integer.valueOf(dpi), Integer.valueOf(dpi),
filename); filename);
if (result == null || result.length == 0) { if (result == null || result.length == 0) {
logger.error("resultant bytes for {} is null, error converting ", filename); logger.error("resultant bytes for {} is null, error converting ", filename);
} }
if (imageFormat.equalsIgnoreCase("webp")) {
// Write the output stream to a temp file
Path tempFile = Files.createTempFile("temp_png", ".png");
try (FileOutputStream fos = new FileOutputStream(tempFile.toFile())) {
fos.write(result);
fos.flush();
}
String pythonVersion = "python3";
try {
ProcessExecutor.getInstance(ProcessExecutor.Processes.PYTHON_OPENCV)
.runCommandWithOutputHandling(Arrays.asList("python3", "--version"));
} catch (IOException e) {
ProcessExecutor.getInstance(ProcessExecutor.Processes.PYTHON_OPENCV)
.runCommandWithOutputHandling(Arrays.asList("python", "--version"));
pythonVersion = "python";
}
List<String> command = new ArrayList<>();
command.add(pythonVersion);
command.add("./scripts/png_to_webp.py"); // Python script to handle the conversion
// Create a temporary directory for the output WebP files
Path tempOutputDir = Files.createTempDirectory("webp_output");
if (singleImage) {
// Run the Python script to convert PNG to WebP
command.add(tempFile.toString());
command.add(tempOutputDir.toString());
command.add("--single");
} else {
// Save the uploaded PDF to a temporary file
Path tempPdfPath = Files.createTempFile("temp_pdf", ".pdf");
file.transferTo(tempPdfPath.toFile());
// Run the Python script to convert PDF to WebP
command.add(tempPdfPath.toString());
command.add(tempOutputDir.toString());
}
command.add("--dpi");
command.add(dpi);
ProcessExecutorResult resultProcess =
ProcessExecutor.getInstance(ProcessExecutor.Processes.PYTHON_OPENCV)
.runCommandWithOutputHandling(command);
// Find all WebP files in the output directory
List<Path> webpFiles =
Files.walk(tempOutputDir)
.filter(path -> path.toString().endsWith(".webp"))
.collect(Collectors.toList());
if (webpFiles.isEmpty()) {
logger.error("No WebP files were created in: {}", tempOutputDir.toString());
throw new IOException("No WebP files were created. " + resultProcess.getMessages());
}
byte[] bodyBytes = new byte[0];
if (webpFiles.size() == 1) {
// Return the single WebP file directly
Path webpFilePath = webpFiles.get(0);
bodyBytes = Files.readAllBytes(webpFilePath);
} else {
// Create a ZIP file containing all WebP images
ByteArrayOutputStream zipOutputStream = new ByteArrayOutputStream();
try (ZipOutputStream zos = new ZipOutputStream(zipOutputStream)) {
for (Path webpFile : webpFiles) {
zos.putNextEntry(new ZipEntry(webpFile.getFileName().toString()));
Files.copy(webpFile, zos);
zos.closeEntry();
}
}
bodyBytes = zipOutputStream.toByteArray();
}
// Clean up the temporary files
Files.deleteIfExists(tempFile);
if (tempOutputDir != null) FileUtils.deleteDirectory(tempOutputDir.toFile());
result = bodyBytes;
}
if (singleImage) { if (singleImage) {
String docName = filename + "." + imageFormat; String docName = filename + "." + imageFormat;
MediaType mediaType = MediaType.parseMediaType(getMediaType(imageFormat)); MediaType mediaType = MediaType.parseMediaType(getMediaType(imageFormat));

View File

@@ -39,6 +39,12 @@ public class ConvertWebsiteToPDF {
if (!URL.matches("^https?://.*") || !GeneralUtils.isValidURL(URL)) { if (!URL.matches("^https?://.*") || !GeneralUtils.isValidURL(URL)) {
throw new IllegalArgumentException("Invalid URL format provided."); throw new IllegalArgumentException("Invalid URL format provided.");
} }
// validate the URL is reachable
if (!GeneralUtils.isURLReachable(URL)) {
throw new IllegalArgumentException("URL is not reachable, please provide a valid URL.");
}
Path tempOutputFile = null; Path tempOutputFile = null;
byte[] pdfBytes; byte[] pdfBytes;
try { try {

View File

@@ -1,12 +1,12 @@
package stirling.software.SPDF.controller.api.misc; package stirling.software.SPDF.controller.api.misc;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.zip.ZipEntry;
import java.util.stream.IntStream; import java.util.zip.ZipOutputStream;
import org.apache.pdfbox.Loader; import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocument;
@@ -17,6 +17,7 @@ import org.apache.pdfbox.text.PDFTextStripper;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
@@ -50,31 +51,31 @@ public class BlankPageController {
int threshold = request.getThreshold(); int threshold = request.getThreshold();
float whitePercent = request.getWhitePercent(); float whitePercent = request.getWhitePercent();
PDDocument document = null; try (PDDocument document = Loader.loadPDF(inputFile.getBytes())) {
try {
document = Loader.loadPDF(inputFile.getBytes());
PDPageTree pages = document.getDocumentCatalog().getPages(); PDPageTree pages = document.getDocumentCatalog().getPages();
PDFTextStripper textStripper = new PDFTextStripper(); PDFTextStripper textStripper = new PDFTextStripper();
List<Integer> pagesToKeepIndex = new ArrayList<>(); List<PDPage> nonBlankPages = new ArrayList<>();
List<PDPage> blankPages = new ArrayList<>();
int pageIndex = 0; int pageIndex = 0;
PDFRenderer pdfRenderer = new PDFRenderer(document); PDFRenderer pdfRenderer = new PDFRenderer(document);
pdfRenderer.setSubsamplingAllowed(true); pdfRenderer.setSubsamplingAllowed(true);
for (PDPage page : pages) { for (PDPage page : pages) {
logger.info("checking page " + pageIndex); logger.info("checking page {}", pageIndex);
textStripper.setStartPage(pageIndex + 1); textStripper.setStartPage(pageIndex + 1);
textStripper.setEndPage(pageIndex + 1); textStripper.setEndPage(pageIndex + 1);
String pageText = textStripper.getText(document); String pageText = textStripper.getText(document);
boolean hasText = !pageText.trim().isEmpty(); boolean hasText = !pageText.trim().isEmpty();
Boolean blank = true; boolean blank = true;
if (hasText) { if (hasText) {
logger.info("page " + pageIndex + " has text, not blank"); logger.info("page {} has text, not blank", pageIndex);
blank = false; blank = false;
} else { } else {
boolean hasImages = PdfUtils.hasImagesOnPage(page); boolean hasImages = PdfUtils.hasImagesOnPage(page);
if (hasImages) { if (hasImages) {
logger.info("page " + pageIndex + " has image, running blank detection"); logger.info("page {} has image, running blank detection", pageIndex);
// Render image and save as temp file // Render image and save as temp file
BufferedImage image = pdfRenderer.renderImageWithDPI(pageIndex, 30); BufferedImage image = pdfRenderer.renderImageWithDPI(pageIndex, 30);
blank = isBlankImage(image, threshold, whitePercent, threshold); blank = isBlankImage(image, threshold, whitePercent, threshold);
@@ -82,34 +83,57 @@ public class BlankPageController {
} }
if (blank) { if (blank) {
logger.info("Skipping, Image was blank for page #" + pageIndex); logger.info("Skipping, Image was blank for page #{}", pageIndex);
blankPages.add(page);
} else { } else {
logger.info("page " + pageIndex + " has image which is not blank"); logger.info("page {} has image which is not blank", pageIndex);
pagesToKeepIndex.add(pageIndex); nonBlankPages.add(page);
} }
pageIndex++; pageIndex++;
} }
// Remove pages not present in pagesToKeepIndex
List<Integer> pageIndices = ByteArrayOutputStream baos = new ByteArrayOutputStream();
IntStream.range(0, pages.getCount()).boxed().collect(Collectors.toList()); ZipOutputStream zos = new ZipOutputStream(baos);
Collections.reverse(pageIndices); // Reverse to prevent index shifting during removal
for (Integer i : pageIndices) { String filename =
if (!pagesToKeepIndex.contains(i)) { Filenames.toSimpleFileName(inputFile.getOriginalFilename())
pages.remove(i); .replaceFirst("[.][^.]+$", "");
}
if (!nonBlankPages.isEmpty()) {
createZipEntry(zos, nonBlankPages, filename + "_nonBlankPages.pdf");
} else {
createZipEntry(zos, blankPages, filename + "_allBlankPages.pdf");
} }
return WebResponseUtils.pdfDocToWebResponse( if (!nonBlankPages.isEmpty() && !blankPages.isEmpty()) {
document, createZipEntry(zos, blankPages, filename + "_blankPages.pdf");
Filenames.toSimpleFileName(inputFile.getOriginalFilename()) }
.replaceFirst("[.][^.]+$", "")
+ "_blanksRemoved.pdf"); zos.close();
logger.info("Returning ZIP file: {}", filename + "_processed.zip");
return WebResponseUtils.boasToWebResponse(
baos, filename + "_processed.zip", MediaType.APPLICATION_OCTET_STREAM);
} catch (IOException e) { } catch (IOException e) {
logger.error("exception", e); logger.error("exception", e);
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
} finally { }
if (document != null) document.close(); }
public void createZipEntry(ZipOutputStream zos, List<PDPage> pages, String entryName)
throws IOException {
try (PDDocument document = new PDDocument()) {
for (PDPage page : pages) {
document.addPage(page);
}
ZipEntry zipEntry = new ZipEntry(entryName);
zos.putNextEntry(zipEntry);
document.save(zos);
zos.closeEntry();
} }
} }

View File

@@ -99,7 +99,7 @@ public class CompressController {
List<String> command = new ArrayList<>(); List<String> command = new ArrayList<>();
command.add("gs"); command.add("gs");
command.add("-sDEVICE=pdfwrite"); command.add("-sDEVICE=pdfwrite");
command.add("-dCompatibilityLevel=1.4"); command.add("-dCompatibilityLevel=1.5");
switch (optimizeLevel) { switch (optimizeLevel) {
case 1: case 1:

View File

@@ -1,13 +1,16 @@
package stirling.software.SPDF.controller.api.misc; package stirling.software.SPDF.controller.api.misc;
import java.awt.Graphics2D; import java.awt.*;
import java.awt.Image;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage; import java.awt.image.RenderedImage;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.zip.Deflater; import java.util.zip.Deflater;
import java.util.zip.ZipEntry; import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream; import java.util.zip.ZipOutputStream;
@@ -49,7 +52,7 @@ public class ExtractImagesController {
description = description =
"This endpoint extracts images from a given PDF file and returns them in a zip file. Users can specify the output image format. Input: PDF Output: IMAGE/ZIP Type: SIMO") "This endpoint extracts images from a given PDF file and returns them in a zip file. Users can specify the output image format. Input: PDF Output: IMAGE/ZIP Type: SIMO")
public ResponseEntity<byte[]> extractImages(@ModelAttribute PDFWithImageFormatRequest request) public ResponseEntity<byte[]> extractImages(@ModelAttribute PDFWithImageFormatRequest request)
throws IOException { throws IOException, InterruptedException, ExecutionException {
MultipartFile file = request.getFileInput(); MultipartFile file = request.getFileInput();
String format = request.getFormat(); String format = request.getFormat();
@@ -57,6 +60,9 @@ public class ExtractImagesController {
System.currentTimeMillis() + " file=" + file.getName() + ", format=" + format); System.currentTimeMillis() + " file=" + file.getName() + ", format=" + format);
PDDocument document = Loader.loadPDF(file.getBytes()); PDDocument document = Loader.loadPDF(file.getBytes());
// Determine if multithreading should be used based on PDF size or number of pages
boolean useMultithreading = shouldUseMultithreading(file, document);
// Create ByteArrayOutputStream to write zip file to byte array // Create ByteArrayOutputStream to write zip file to byte array
ByteArrayOutputStream baos = new ByteArrayOutputStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream();
@@ -66,71 +72,51 @@ public class ExtractImagesController {
// Set compression level // Set compression level
zos.setLevel(Deflater.BEST_COMPRESSION); zos.setLevel(Deflater.BEST_COMPRESSION);
int imageIndex = 1;
String filename = String filename =
Filenames.toSimpleFileName(file.getOriginalFilename()) Filenames.toSimpleFileName(file.getOriginalFilename())
.replaceFirst("[.][^.]+$", ""); .replaceFirst("[.][^.]+$", "");
int pageNum = 0;
Set<Integer> processedImages = new HashSet<>(); Set<Integer> processedImages = new HashSet<>();
if (useMultithreading) {
// Executor service to handle multithreading
ExecutorService executor =
Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
Set<Future<Void>> futures = new HashSet<>();
// Iterate over each page // Iterate over each page
for (PDPage page : document.getPages()) { for (int pgNum = 0; pgNum < document.getPages().getCount(); pgNum++) {
++pageNum; PDPage page = document.getPage(pgNum);
// Extract images from page int pageNum = document.getPages().indexOf(page) + 1;
for (COSName name : page.getResources().getXObjectNames()) { // Submit a task for processing each page
if (page.getResources().isImageXObject(name)) { Future<Void> future =
PDImageXObject image = (PDImageXObject) page.getResources().getXObject(name); executor.submit(
int imageHash = image.hashCode(); () -> {
if (processedImages.contains(imageHash)) { extractImagesFromPage(
continue; // Skip already processed images page, format, filename, pageNum, processedImages, zos);
} return null;
processedImages.add(imageHash); });
// Convert image to desired format futures.add(future);
RenderedImage renderedImage = image.getImage();
BufferedImage bufferedImage = null;
if ("png".equalsIgnoreCase(format)) {
bufferedImage =
new BufferedImage(
renderedImage.getWidth(),
renderedImage.getHeight(),
BufferedImage.TYPE_INT_ARGB);
} else if ("jpeg".equalsIgnoreCase(format) || "jpg".equalsIgnoreCase(format)) {
bufferedImage =
new BufferedImage(
renderedImage.getWidth(),
renderedImage.getHeight(),
BufferedImage.TYPE_INT_RGB);
} else if ("gif".equalsIgnoreCase(format)) {
bufferedImage =
new BufferedImage(
renderedImage.getWidth(),
renderedImage.getHeight(),
BufferedImage.TYPE_BYTE_INDEXED);
} }
// Write image to zip file // Wait for all tasks to complete
String imageName = for (Future<Void> future : futures) {
filename + "_" + imageIndex + " (Page " + pageNum + ")." + format; future.get();
ZipEntry zipEntry = new ZipEntry(imageName);
zos.putNextEntry(zipEntry);
Graphics2D g = bufferedImage.createGraphics();
g.drawImage((Image) renderedImage, 0, 0, null);
g.dispose();
// Write image bytes to zip file
ByteArrayOutputStream imageBaos = new ByteArrayOutputStream();
ImageIO.write(bufferedImage, format, imageBaos);
zos.write(imageBaos.toByteArray());
zos.closeEntry();
imageIndex++;
} }
// Close executor service
executor.shutdown();
} else {
// Single-threaded extraction
for (int pgNum = 0; pgNum < document.getPages().getCount(); pgNum++) {
PDPage page = document.getPage(pgNum);
extractImagesFromPage(page, format, filename, pgNum + 1, processedImages, zos);
} }
} }
// Close ZipOutputStream and PDDocument // Close PDDocument and ZipOutputStream
zos.close();
document.close(); document.close();
zos.close();
// Create ByteArrayResource from byte array // Create ByteArrayResource from byte array
byte[] zipContents = baos.toByteArray(); byte[] zipContents = baos.toByteArray();
@@ -138,4 +124,72 @@ public class ExtractImagesController {
return WebResponseUtils.boasToWebResponse( return WebResponseUtils.boasToWebResponse(
baos, filename + "_extracted-images.zip", MediaType.APPLICATION_OCTET_STREAM); baos, filename + "_extracted-images.zip", MediaType.APPLICATION_OCTET_STREAM);
} }
private boolean shouldUseMultithreading(MultipartFile file, PDDocument document) {
// Criteria: Use multithreading if file size > 10MB or number of pages > 20
long fileSizeInMB = file.getSize() / (1024 * 1024);
int numberOfPages = document.getPages().getCount();
return fileSizeInMB > 10 || numberOfPages > 20;
}
private void extractImagesFromPage(
PDPage page,
String format,
String filename,
int pageNum,
Set<Integer> processedImages,
ZipOutputStream zos)
throws IOException {
if(page.getResources() == null || page.getResources().getXObjectNames() == null) {
return;
}
for (COSName name : page.getResources().getXObjectNames()) {
if (page.getResources().isImageXObject(name)) {
PDImageXObject image = (PDImageXObject) page.getResources().getXObject(name);
int imageHash = image.hashCode();
synchronized (processedImages) {
if (processedImages.contains(imageHash)) {
continue; // Skip already processed images
}
processedImages.add(imageHash);
}
RenderedImage renderedImage = image.getImage();
// Convert to standard RGB colorspace if needed
BufferedImage bufferedImage = convertToRGB(renderedImage, format);
// Write image to zip file
String imageName = filename + "_" + imageHash + " (Page " + pageNum + ")." + format;
synchronized (zos) {
zos.putNextEntry(new ZipEntry(imageName));
ByteArrayOutputStream imageBaos = new ByteArrayOutputStream();
ImageIO.write(bufferedImage, format, imageBaos);
zos.write(imageBaos.toByteArray());
zos.closeEntry();
}
}
}
}
private BufferedImage convertToRGB(RenderedImage renderedImage, String format) {
int width = renderedImage.getWidth();
int height = renderedImage.getHeight();
BufferedImage rgbImage;
if ("png".equalsIgnoreCase(format)) {
rgbImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
} else if ("jpeg".equalsIgnoreCase(format) || "jpg".equalsIgnoreCase(format)) {
rgbImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
} else if ("gif".equalsIgnoreCase(format)) {
rgbImage = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_INDEXED);
} else {
rgbImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
}
Graphics2D g = rgbImage.createGraphics();
g.drawImage((Image) renderedImage, 0, 0, null);
g.dispose();
return rgbImage;
}
} }

View File

@@ -13,8 +13,7 @@ import java.util.stream.Collectors;
import java.util.zip.ZipEntry; import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream; import java.util.zip.ZipOutputStream;
import org.slf4j.Logger; import org.springframework.beans.factory.annotation.Autowired;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.ModelAttribute;
@@ -27,6 +26,7 @@ import io.github.pixee.security.Filenames;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.api.misc.ProcessPdfWithOcrRequest; import stirling.software.SPDF.model.api.misc.ProcessPdfWithOcrRequest;
import stirling.software.SPDF.utils.ProcessExecutor; import stirling.software.SPDF.utils.ProcessExecutor;
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult; import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
@@ -37,10 +37,10 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@Tag(name = "Misc", description = "Miscellaneous APIs") @Tag(name = "Misc", description = "Miscellaneous APIs")
public class OCRController { public class OCRController {
private static final Logger logger = LoggerFactory.getLogger(OCRController.class); @Autowired ApplicationProperties applicationProperties;
public List<String> getAvailableTesseractLanguages() { public List<String> getAvailableTesseractLanguages() {
String tessdataDir = "/usr/share/tessdata"; String tessdataDir = applicationProperties.getSystem().getTessdataDir();
File[] files = new File(tessdataDir).listFiles(); File[] files = new File(tessdataDir).listFiles();
if (files == null) { if (files == null) {
return Collections.emptyList(); return Collections.emptyList();

View File

@@ -1,21 +1,14 @@
package stirling.software.SPDF.controller.api.security; package stirling.software.SPDF.controller.api.security;
import java.awt.Color; import java.awt.Color;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.List;
import org.apache.pdfbox.Loader; import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream; import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.PDPageContentStream.AppendMode;
import org.apache.pdfbox.pdmodel.common.PDRectangle; import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.graphics.image.LosslessFactory;
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
import org.apache.pdfbox.rendering.ImageType;
import org.apache.pdfbox.rendering.PDFRenderer;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
@@ -32,6 +25,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.PDFText; import stirling.software.SPDF.model.PDFText;
import stirling.software.SPDF.model.api.security.RedactPdfRequest; import stirling.software.SPDF.model.api.security.RedactPdfRequest;
import stirling.software.SPDF.pdf.TextFinder; import stirling.software.SPDF.pdf.TextFinder;
import stirling.software.SPDF.utils.PdfUtils;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@@ -81,22 +75,9 @@ public class RedactController {
} }
if (convertPDFToImage) { if (convertPDFToImage) {
PDDocument imageDocument = new PDDocument(); PDDocument convertedPdf = PdfUtils.convertPdfToPdfImage(document);
PDFRenderer pdfRenderer = new PDFRenderer(document);
pdfRenderer.setSubsamplingAllowed(true);
for (int page = 0; page < document.getNumberOfPages(); ++page) {
BufferedImage bim = pdfRenderer.renderImageWithDPI(page, 300, ImageType.RGB);
PDPage newPage = new PDPage(new PDRectangle(bim.getWidth(), bim.getHeight()));
imageDocument.addPage(newPage);
PDImageXObject pdImage = LosslessFactory.createFromImage(imageDocument, bim);
PDPageContentStream contentStream =
new PDPageContentStream(
imageDocument, newPage, AppendMode.APPEND, true, true);
contentStream.drawImage(pdImage, 0, 0);
contentStream.close();
}
document.close(); document.close();
document = imageDocument; document = convertedPdf;
} }
ByteArrayOutputStream baos = new ByteArrayOutputStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream();

View File

@@ -36,6 +36,7 @@ import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.security.AddWatermarkRequest; import stirling.software.SPDF.model.api.security.AddWatermarkRequest;
import stirling.software.SPDF.utils.PdfUtils;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@@ -60,6 +61,7 @@ public class WatermarkController {
float opacity = request.getOpacity(); float opacity = request.getOpacity();
int widthSpacer = request.getWidthSpacer(); int widthSpacer = request.getWidthSpacer();
int heightSpacer = request.getHeightSpacer(); int heightSpacer = request.getHeightSpacer();
boolean convertPdfToImage = request.isConvertPDFToImage();
// Load the input PDF // Load the input PDF
PDDocument document = Loader.loadPDF(pdfFile.getBytes()); PDDocument document = Loader.loadPDF(pdfFile.getBytes());
@@ -104,6 +106,12 @@ public class WatermarkController {
contentStream.close(); contentStream.close();
} }
if (convertPdfToImage) {
PDDocument convertedPdf = PdfUtils.convertPdfToPdfImage(document);
document.close();
document = convertedPdf;
}
return WebResponseUtils.pdfDocToWebResponse( return WebResponseUtils.pdfDocToWebResponse(
document, document,
Filenames.toSimpleFileName(pdfFile.getOriginalFilename()) Filenames.toSimpleFileName(pdfFile.getOriginalFilename())

View File

@@ -1,13 +1,15 @@
package stirling.software.SPDF.controller.web; package stirling.software.SPDF.controller.web;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
@@ -23,11 +25,14 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2; import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2.Client; import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2.Client;
import stirling.software.SPDF.model.Authority; import stirling.software.SPDF.model.Authority;
import stirling.software.SPDF.model.Role; import stirling.software.SPDF.model.Role;
import stirling.software.SPDF.model.SessionEntity;
import stirling.software.SPDF.model.User; import stirling.software.SPDF.model.User;
import stirling.software.SPDF.model.provider.GithubProvider; import stirling.software.SPDF.model.provider.GithubProvider;
import stirling.software.SPDF.model.provider.GoogleProvider; import stirling.software.SPDF.model.provider.GoogleProvider;
@@ -35,15 +40,20 @@ import stirling.software.SPDF.model.provider.KeycloakProvider;
import stirling.software.SPDF.repository.UserRepository; import stirling.software.SPDF.repository.UserRepository;
@Controller @Controller
@Slf4j
@Tag(name = "Account Security", description = "Account Security APIs") @Tag(name = "Account Security", description = "Account Security APIs")
public class AccountWebController { public class AccountWebController {
@Autowired ApplicationProperties applicationProperties; @Autowired ApplicationProperties applicationProperties;
private static final Logger logger = LoggerFactory.getLogger(AccountWebController.class); @Autowired SessionPersistentRegistry sessionPersistentRegistry;
@Autowired
private UserRepository userRepository; // Assuming you have a repository for user operations
@GetMapping("/login") @GetMapping("/login")
public String login(HttpServletRequest request, Model model, Authentication authentication) { public String login(HttpServletRequest request, Model model, Authentication authentication) {
// If the user is already authenticated, redirect them to the home page.
if (authentication != null && authentication.isAuthenticated()) { if (authentication != null && authentication.isAuthenticated()) {
return "redirect:/"; return "redirect:/";
} }
@@ -137,6 +147,13 @@ public class AccountWebController {
break; break;
case "invalid_id_token": case "invalid_id_token":
erroroauth = "login.oauth2InvalidIdToken"; erroroauth = "login.oauth2InvalidIdToken";
break;
case "oauth2_admin_blocked_user":
erroroauth = "login.oauth2AdminBlockedUser";
break;
case "userIsDisabled":
erroroauth = "login.userIsDisabled";
break;
default: default:
break; break;
} }
@@ -155,9 +172,6 @@ public class AccountWebController {
return "login"; return "login";
} }
@Autowired
private UserRepository userRepository; // Assuming you have a repository for user operations
@PreAuthorize("hasRole('ROLE_ADMIN')") @PreAuthorize("hasRole('ROLE_ADMIN')")
@GetMapping("/addUsers") @GetMapping("/addUsers")
public String showAddUserForm( public String showAddUserForm(
@@ -166,6 +180,13 @@ public class AccountWebController {
Iterator<User> iterator = allUsers.iterator(); Iterator<User> iterator = allUsers.iterator();
Map<String, String> roleDetails = Role.getAllRoleDetails(); Map<String, String> roleDetails = Role.getAllRoleDetails();
// Map to store session information and user activity status
Map<String, Boolean> userSessions = new HashMap<>();
Map<String, Date> userLastRequest = new HashMap<>();
int activeUsers = 0;
int disabledUsers = 0;
while (iterator.hasNext()) { while (iterator.hasNext()) {
User user = iterator.next(); User user = iterator.next();
if (user != null) { if (user != null) {
@@ -176,8 +197,72 @@ public class AccountWebController {
break; // Break out of the inner loop once the user is removed break; // Break out of the inner loop once the user is removed
} }
} }
// Determine the user's session status and last request time
int maxInactiveInterval = sessionPersistentRegistry.getMaxInactiveInterval();
boolean hasActiveSession = false;
Date lastRequest = null;
Optional<SessionEntity> latestSession =
sessionPersistentRegistry.findLatestSession(user.getUsername());
if (latestSession.isPresent()) {
SessionEntity sessionEntity = latestSession.get();
Date lastAccessedTime = sessionEntity.getLastRequest();
Instant now = Instant.now();
// Calculate session expiration and update session status accordingly
Instant expirationTime =
lastAccessedTime
.toInstant()
.plus(maxInactiveInterval, ChronoUnit.SECONDS);
if (now.isAfter(expirationTime)) {
sessionPersistentRegistry.expireSession(sessionEntity.getSessionId());
hasActiveSession = false;
} else {
hasActiveSession = !sessionEntity.isExpired();
}
lastRequest = sessionEntity.getLastRequest();
} else {
hasActiveSession = false;
lastRequest = new Date(0); // No session, set default last request time
}
userSessions.put(user.getUsername(), hasActiveSession);
userLastRequest.put(user.getUsername(), lastRequest);
if (hasActiveSession) {
activeUsers++;
}
if (!user.isEnabled()) {
disabledUsers++;
} }
} }
}
// Sort users by active status and last request date
List<User> sortedUsers =
allUsers.stream()
.sorted(
(u1, u2) -> {
boolean u1Active = userSessions.get(u1.getUsername());
boolean u2Active = userSessions.get(u2.getUsername());
if (u1Active && !u2Active) {
return -1;
} else if (!u1Active && u2Active) {
return 1;
} else {
Date u1LastRequest =
userLastRequest.getOrDefault(
u1.getUsername(), new Date(0));
Date u2LastRequest =
userLastRequest.getOrDefault(
u2.getUsername(), new Date(0));
return u2LastRequest.compareTo(u1LastRequest);
}
})
.collect(Collectors.toList());
String messageType = request.getParameter("messageType"); String messageType = request.getParameter("messageType");
@@ -203,6 +288,9 @@ public class AccountWebController {
case "invalidUsername": case "invalidUsername":
addMessage = "invalidUsernameMessage"; addMessage = "invalidUsernameMessage";
break; break;
case "invalidPassword":
addMessage = "invalidPasswordMessage";
break;
default: default:
break; break;
} }
@@ -218,16 +306,24 @@ public class AccountWebController {
case "downgradeCurrentUser": case "downgradeCurrentUser":
changeMessage = "downgradeCurrentUserMessage"; changeMessage = "downgradeCurrentUserMessage";
break; break;
case "disabledCurrentUser":
changeMessage = "disabledCurrentUserMessage";
break;
default: default:
changeMessage = messageType;
break; break;
} }
model.addAttribute("changeMessage", changeMessage); model.addAttribute("changeMessage", changeMessage);
} }
model.addAttribute("users", allUsers); model.addAttribute("users", sortedUsers);
model.addAttribute("currentUsername", authentication.getName()); model.addAttribute("currentUsername", authentication.getName());
model.addAttribute("roleDetails", roleDetails); model.addAttribute("roleDetails", roleDetails);
model.addAttribute("userSessions", userSessions);
model.addAttribute("userLastRequest", userLastRequest);
model.addAttribute("totalUsers", allUsers.size());
model.addAttribute("activeUsers", activeUsers);
model.addAttribute("disabledUsers", disabledUsers);
return "addUsers"; return "addUsers";
} }
@@ -278,7 +374,7 @@ public class AccountWebController {
settingsJson = objectMapper.writeValueAsString(user.get().getSettings()); settingsJson = objectMapper.writeValueAsString(user.get().getSettings());
} catch (JsonProcessingException e) { } catch (JsonProcessingException e) {
// Handle JSON conversion error // Handle JSON conversion error
logger.error("exception", e); log.error("exception", e);
return "redirect:/error"; return "redirect:/error";
} }

View File

@@ -21,14 +21,6 @@ public class ConverterWebController {
return "convert/book-to-pdf"; return "convert/book-to-pdf";
} }
@ConditionalOnExpression("#{bookAndHtmlFormatsInstalled}")
@GetMapping("/pdf-to-book")
@Hidden
public String convertPdfToBookForm(Model model) {
model.addAttribute("currentPage", "pdf-to-book");
return "convert/pdf-to-book";
}
@GetMapping("/img-to-pdf") @GetMapping("/img-to-pdf")
@Hidden @Hidden
public String convertImgToPdfForm(Model model) { public String convertImgToPdfForm(Model model) {
@@ -57,13 +49,6 @@ public class ConverterWebController {
return "convert/url-to-pdf"; return "convert/url-to-pdf";
} }
@GetMapping("/pdf-to-img")
@Hidden
public String pdfToimgForm(Model model) {
model.addAttribute("currentPage", "pdf-to-img");
return "convert/pdf-to-img";
}
@GetMapping("/file-to-pdf") @GetMapping("/file-to-pdf")
@Hidden @Hidden
public String convertToPdfForm(Model model) { public String convertToPdfForm(Model model) {
@@ -73,6 +58,21 @@ public class ConverterWebController {
// PDF TO...... // PDF TO......
@ConditionalOnExpression("#{bookAndHtmlFormatsInstalled}")
@GetMapping("/pdf-to-book")
@Hidden
public String convertPdfToBookForm(Model model) {
model.addAttribute("currentPage", "pdf-to-book");
return "convert/pdf-to-book";
}
@GetMapping("/pdf-to-img")
@Hidden
public String pdfToimgForm(Model model) {
model.addAttribute("currentPage", "pdf-to-img");
return "convert/pdf-to-img";
}
@GetMapping("/pdf-to-html") @GetMapping("/pdf-to-html")
@Hidden @Hidden
public ModelAndView pdfToHTML() { public ModelAndView pdfToHTML() {

View File

@@ -310,4 +310,11 @@ public class GeneralWebController {
model.addAttribute("currentPage", "auto-split-pdf"); model.addAttribute("currentPage", "auto-split-pdf");
return "auto-split-pdf"; return "auto-split-pdf";
} }
@GetMapping("/remove-image-pdf")
@Hidden
public String removeImagePdfForm(Model model) {
model.addAttribute("currentPage", "remove-image-pdf");
return "remove-image-pdf";
}
} }

View File

@@ -6,6 +6,7 @@ import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.ui.Model; import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
@@ -14,10 +15,14 @@ import org.springframework.web.servlet.ModelAndView;
import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.ApplicationProperties;
@Controller @Controller
@Tag(name = "Misc", description = "Miscellaneous APIs") @Tag(name = "Misc", description = "Miscellaneous APIs")
public class OtherWebController { public class OtherWebController {
@Autowired ApplicationProperties applicationProperties;
@GetMapping("/compress-pdf") @GetMapping("/compress-pdf")
@Hidden @Hidden
public String compressPdfForm(Model model) { public String compressPdfForm(Model model) {
@@ -97,7 +102,7 @@ public class OtherWebController {
} }
public List<String> getAvailableTesseractLanguages() { public List<String> getAvailableTesseractLanguages() {
String tessdataDir = "/usr/share/tessdata"; String tessdataDir = applicationProperties.getSystem().getTessdataDir();
File[] files = new File(tessdataDir).listFiles(); File[] files = new File(tessdataDir).listFiles();
if (files == null) { if (files == null) {
return Collections.emptyList(); return Collections.emptyList();

View File

@@ -241,6 +241,7 @@ public class ApplicationProperties {
private String clientId; private String clientId;
private String clientSecret; private String clientSecret;
private Boolean autoCreateUser = false; private Boolean autoCreateUser = false;
private Boolean blockRegistration = false;
private String useAsUsername; private String useAsUsername;
private Collection<String> scopes = new ArrayList<>(); private Collection<String> scopes = new ArrayList<>();
private String provider; private String provider;
@@ -286,6 +287,14 @@ public class ApplicationProperties {
this.autoCreateUser = autoCreateUser; this.autoCreateUser = autoCreateUser;
} }
public Boolean getBlockRegistration() {
return blockRegistration;
}
public void setBlockRegistration(Boolean blockRegistration) {
this.blockRegistration = blockRegistration;
}
public String getUseAsUsername() { public String getUseAsUsername() {
return useAsUsername; return useAsUsername;
} }
@@ -356,10 +365,14 @@ public class ApplicationProperties {
+ (clientSecret != null && !clientSecret.isEmpty() ? "MASKED" : "NULL") + (clientSecret != null && !clientSecret.isEmpty() ? "MASKED" : "NULL")
+ ", autoCreateUser=" + ", autoCreateUser="
+ autoCreateUser + autoCreateUser
+ ", blockRegistration="
+ blockRegistration
+ ", useAsUsername=" + ", useAsUsername="
+ useAsUsername + useAsUsername
+ ", provider=" + ", provider="
+ provider + provider
+ ", client="
+ client
+ ", scopes=" + ", scopes="
+ scopes + scopes
+ "]"; + "]";
@@ -429,6 +442,15 @@ public class ApplicationProperties {
private boolean showUpdate; private boolean showUpdate;
private Boolean showUpdateOnlyAdmin; private Boolean showUpdateOnlyAdmin;
private boolean customHTMLFiles; private boolean customHTMLFiles;
private String tessdataDir;
public String getTessdataDir() {
return tessdataDir;
}
public void setTessdataDir(String tessdataDir) {
this.tessdataDir = tessdataDir;
}
public boolean isCustomHTMLFiles() { public boolean isCustomHTMLFiles() {
return customHTMLFiles; return customHTMLFiles;

View File

@@ -1,5 +1,7 @@
package stirling.software.SPDF.model; package stirling.software.SPDF.model;
import java.io.Serializable;
import jakarta.persistence.Column; import jakarta.persistence.Column;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue; import jakarta.persistence.GeneratedValue;
@@ -11,7 +13,9 @@ import jakarta.persistence.Table;
@Entity @Entity
@Table(name = "authorities") @Table(name = "authorities")
public class Authority { public class Authority implements Serializable {
private static final long serialVersionUID = 1L;
public Authority() {} public Authority() {}

View File

@@ -0,0 +1,23 @@
package stirling.software.SPDF.model;
import java.io.Serializable;
import java.util.Date;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Lob;
import jakarta.persistence.Table;
import lombok.Data;
@Entity
@Data
@Table(name = "sessions")
public class SessionEntity implements Serializable {
@Id private String sessionId;
@Lob private String principalName;
private Date lastRequest;
private boolean expired;
}

View File

@@ -1,5 +1,6 @@
package stirling.software.SPDF.model; package stirling.software.SPDF.model;
import java.io.Serializable;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.Map; import java.util.Map;
@@ -23,7 +24,9 @@ import jakarta.persistence.Table;
@Entity @Entity
@Table(name = "users") @Table(name = "users")
public class User { public class User implements Serializable {
private static final long serialVersionUID = 1L;
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)

View File

@@ -12,7 +12,7 @@ public class ConvertToImageRequest extends PDFFile {
@Schema( @Schema(
description = "The output image format", description = "The output image format",
allowableValues = {"png", "jpeg", "jpg", "gif"}) allowableValues = {"png", "jpeg", "jpg", "gif", "webp"})
private String imageFormat; private String imageFormat;
@Schema( @Schema(

View File

@@ -44,4 +44,7 @@ public class AddWatermarkRequest extends PDFFile {
@Schema(description = "The height spacer between watermark elements", example = "50") @Schema(description = "The height spacer between watermark elements", example = "50")
private int heightSpacer; private int heightSpacer;
@Schema(description = "Convert the redacted PDF to an image", defaultValue = "false")
private boolean convertPDFToImage;
} }

View File

@@ -31,6 +31,14 @@ public class GoogleProvider extends Provider {
private Collection<String> scopes = new ArrayList<>(); private Collection<String> scopes = new ArrayList<>();
private String useAsUsername = "email"; private String useAsUsername = "email";
@Override
public String getIssuer() {
return new String();
}
@Override
public void setIssuer(String issuer) {}
@Override @Override
public String getClientId() { public String getClientId() {
return this.clientId; return this.clientId;

View File

@@ -3,9 +3,11 @@ package stirling.software.SPDF.repository;
import java.util.Set; import java.util.Set;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import stirling.software.SPDF.model.Authority; import stirling.software.SPDF.model.Authority;
@Repository
public interface AuthorityRepository extends JpaRepository<Authority, Long> { public interface AuthorityRepository extends JpaRepository<Authority, Long> {
// Set<Authority> findByUsername(String username); // Set<Authority> findByUsername(String username);
Set<Authority> findByUser_Username(String username); Set<Authority> findByUser_Username(String username);

View File

@@ -1,7 +1,9 @@
package stirling.software.SPDF.repository; package stirling.software.SPDF.repository;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import stirling.software.SPDF.model.PersistentLogin; import stirling.software.SPDF.model.PersistentLogin;
@Repository
public interface PersistentLoginRepository extends JpaRepository<PersistentLogin, String> {} public interface PersistentLoginRepository extends JpaRepository<PersistentLogin, String> {}

View File

@@ -3,13 +3,15 @@ package stirling.software.SPDF.repository;
import java.util.Optional; import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import stirling.software.SPDF.model.User; import stirling.software.SPDF.model.User;
public interface UserRepository extends JpaRepository<User, String> { @Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsernameIgnoreCase(String username); Optional<User> findByUsernameIgnoreCase(String username);
Optional<User> findByUsername(String username); Optional<User> findByUsername(String username);
User findByApiKey(String apiKey); Optional<User> findByApiKey(String apiKey);
} }

View File

@@ -0,0 +1,51 @@
package stirling.software.SPDF.service;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDResources;
import org.apache.pdfbox.pdmodel.graphics.PDXObject;
import org.springframework.stereotype.Service;
/** Service class responsible for removing image objects from a PDF document. */
@Service
public class PdfImageRemovalService {
/**
* Removes all image objects from the provided PDF document.
*
* <p>This method iterates over each page in the document and removes any image XObjects found
* in the page's resources.
*
* @param document The PDF document from which images will be removed.
* @return The modified PDF document with images removed.
* @throws IOException If an error occurs while processing the PDF document.
*/
public PDDocument removeImagesFromPdf(PDDocument document) throws IOException {
// Iterate over each page in the PDF document
for (PDPage page : document.getPages()) {
PDResources resources = page.getResources();
// Collect the XObject names to remove
List<COSName> namesToRemove = new ArrayList<>();
// Iterate over all XObject names in the page's resources
for (COSName name : resources.getXObjectNames()) {
// Check if the XObject is an image
if (resources.isImageXObject(name)) {
// Collect the name for removal
namesToRemove.add(name);
}
}
// Now, modify the resources by removing the collected names
for (COSName name : namesToRemove) {
resources.put(name, (PDXObject) null);
}
}
return document;
}
}

View File

@@ -13,6 +13,8 @@ import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.net.URL;
import java.net.HttpURLConnection;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -71,6 +73,21 @@ public class GeneralUtils {
} catch (MalformedURLException e) { } catch (MalformedURLException e) {
return false; return false;
} }
}
public static boolean isURLReachable(String urlStr) {
try {
URL url = new URL(urlStr);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("HEAD");
int responseCode = connection.getResponseCode();
return (200 <= responseCode && responseCode <= 399);
} catch (MalformedURLException e) {
return false;
} catch (IOException e) {
return false;
}
} }
public static File multipartToFile(MultipartFile multipart) throws IOException { public static File multipartToFile(MultipartFile multipart) throws IOException {
@@ -95,16 +112,13 @@ public class GeneralUtils {
sizeStr = sizeStr.replace(",", ".").replace(" ", ""); sizeStr = sizeStr.replace(",", ".").replace(" ", "");
try { try {
if (sizeStr.endsWith("KB")) { if (sizeStr.endsWith("KB")) {
return (long) return (long) (Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2)) * 1024);
(Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2)) * 1024);
} else if (sizeStr.endsWith("MB")) { } else if (sizeStr.endsWith("MB")) {
return (long) return (long) (Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2))
(Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2))
* 1024 * 1024
* 1024); * 1024);
} else if (sizeStr.endsWith("GB")) { } else if (sizeStr.endsWith("GB")) {
return (long) return (long) (Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2))
(Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2))
* 1024 * 1024
* 1024 * 1024
* 1024); * 1024);
@@ -170,13 +184,15 @@ public class GeneralUtils {
int n = 0; int n = 0;
while (true) { while (true) {
// Replace 'n' with the current value of n, correctly handling numbers before 'n' // Replace 'n' with the current value of n, correctly handling numbers before
// 'n'
String sanitizedExpression = insertMultiplicationBeforeN(expression, n); String sanitizedExpression = insertMultiplicationBeforeN(expression, n);
Double result = evaluator.evaluate(sanitizedExpression); Double result = evaluator.evaluate(sanitizedExpression);
// Check if the result is null or not within bounds // Check if the result is null or not within bounds
if (result == null || result <= 0 || result.intValue() > maxValue) { if (result == null || result <= 0 || result.intValue() > maxValue) {
if (n != 0) break; if (n != 0)
break;
} else { } else {
results.add(result.intValue()); results.add(result.intValue());
} }

View File

@@ -341,6 +341,30 @@ public class PdfUtils {
} }
} }
/**
* Converts a given Pdf file to PDF-Image.
*
* @param document to be converted. Note: the caller is responsible for closing the document
* @return converted document to PDF-Image
* @throws IOException if conversion fails
*/
public static PDDocument convertPdfToPdfImage(PDDocument document) throws IOException {
PDDocument imageDocument = new PDDocument();
PDFRenderer pdfRenderer = new PDFRenderer(document);
pdfRenderer.setSubsamplingAllowed(true);
for (int page = 0; page < document.getNumberOfPages(); ++page) {
BufferedImage bim = pdfRenderer.renderImageWithDPI(page, 300, ImageType.RGB);
PDPage newPage = new PDPage(new PDRectangle(bim.getWidth(), bim.getHeight()));
imageDocument.addPage(newPage);
PDImageXObject pdImage = LosslessFactory.createFromImage(imageDocument, bim);
PDPageContentStream contentStream =
new PDPageContentStream(imageDocument, newPage, AppendMode.APPEND, true, true);
contentStream.drawImage(pdImage, 0, 0);
contentStream.close();
}
return imageDocument;
}
private static BufferedImage prepareImageForPdfToImage( private static BufferedImage prepareImageForPdfToImage(
int maxWidth, int height, String imageType) { int maxWidth, int height, String imageType) {
BufferedImage combined; BufferedImage combined;

View File

@@ -55,10 +55,12 @@ userNotFoundMessage=User not found.
incorrectPasswordMessage=Current password is incorrect. incorrectPasswordMessage=Current password is incorrect.
usernameExistsMessage=New Username already exists. usernameExistsMessage=New Username already exists.
invalidUsernameMessage=Invalid username, username can only contain letters, numbers and the following special characters @._+- or must be a valid email address. invalidUsernameMessage=Invalid username, username can only contain letters, numbers and the following special characters @._+- or must be a valid email address.
invalidPasswordMessage=The password must not be empty and must not have spaces at the beginning or end.
confirmPasswordErrorMessage=New Password and Confirm New Password must match. confirmPasswordErrorMessage=New Password and Confirm New Password must match.
deleteCurrentUserMessage=Cannot delete currently logged in user. deleteCurrentUserMessage=Cannot delete currently logged in user.
deleteUsernameExistsMessage=The username does not exist and cannot be deleted. deleteUsernameExistsMessage=The username does not exist and cannot be deleted.
downgradeCurrentUserMessage=لا يمكن خفض دور المستخدم الحالي downgradeCurrentUserMessage=لا يمكن خفض دور المستخدم الحالي
disabledCurrentUserMessage=The current user cannot be disabled
downgradeCurrentUserLongMessage=لا يمكن تخفيض دور المستخدم الحالي. وبالتالي، لن يظهر المستخدم الحالي. downgradeCurrentUserLongMessage=لا يمكن تخفيض دور المستخدم الحالي. وبالتالي، لن يظهر المستخدم الحالي.
userAlreadyExistsOAuthMessage=The user already exists as an OAuth2 user. userAlreadyExistsOAuthMessage=The user already exists as an OAuth2 user.
userAlreadyExistsWebMessage=The user already exists as an web user. userAlreadyExistsWebMessage=The user already exists as an web user.
@@ -177,6 +179,7 @@ adminUserSettings.user=User
adminUserSettings.addUser=Add New User adminUserSettings.addUser=Add New User
adminUserSettings.deleteUser=Delete User adminUserSettings.deleteUser=Delete User
adminUserSettings.confirmDeleteUser=Should the user be deleted? adminUserSettings.confirmDeleteUser=Should the user be deleted?
adminUserSettings.confirmChangeUserStatus=Should the user be disabled/enabled?
adminUserSettings.usernameInfo=Username can only contain letters, numbers and the following special characters @._+- or must be a valid email address. adminUserSettings.usernameInfo=Username can only contain letters, numbers and the following special characters @._+- or must be a valid email address.
adminUserSettings.roles=Roles adminUserSettings.roles=Roles
adminUserSettings.role=Role adminUserSettings.role=Role
@@ -190,6 +193,13 @@ adminUserSettings.forceChange=Force user to change password on login
adminUserSettings.submit=Save User adminUserSettings.submit=Save User
adminUserSettings.changeUserRole=تغيير دور المستخدم adminUserSettings.changeUserRole=تغيير دور المستخدم
adminUserSettings.authenticated=Authenticated adminUserSettings.authenticated=Authenticated
adminUserSettings.editOwnProfil=Edit own profile
adminUserSettings.enabledUser=enabled user
adminUserSettings.disabledUser=disabled user
adminUserSettings.activeUsers=Active Users:
adminUserSettings.disabledUsers=Disabled Users:
adminUserSettings.totalUsers=Total Users:
adminUserSettings.lastRequest=Last Request
database.title=Database Import/Export database.title=Database Import/Export
@@ -461,6 +471,10 @@ home.BookToPDF.title=Book to PDF
home.BookToPDF.desc=Converts Books/Comics formats to PDF using calibre home.BookToPDF.desc=Converts Books/Comics formats to PDF using calibre
BookToPDF.tags=Book,Comic,Calibre,Convert,manga,amazon,kindle BookToPDF.tags=Book,Comic,Calibre,Convert,manga,amazon,kindle
home.removeImagePdf.title=Remove image
home.removeImagePdf.desc=Remove image from PDF to reduce file size
removeImagePdf.tags=Remove Image,Page operations,Back end,server side
########################### ###########################
# # # #
@@ -477,12 +491,14 @@ login.locked=Your account has been locked.
login.signinTitle=Please sign in login.signinTitle=Please sign in
login.ssoSignIn=تسجيل الدخول عبر تسجيل الدخول الأحادي login.ssoSignIn=تسجيل الدخول عبر تسجيل الدخول الأحادي
login.oauth2AutoCreateDisabled=تم تعطيل مستخدم الإنشاء التلقائي لـ OAuth2 login.oauth2AutoCreateDisabled=تم تعطيل مستخدم الإنشاء التلقائي لـ OAuth2
login.oauth2AdminBlockedUser=Registration or logging in of non-registered users is currently blocked. Please contact the administrator.
login.oauth2RequestNotFound=Authorization request not found login.oauth2RequestNotFound=Authorization request not found
login.oauth2InvalidUserInfoResponse=Invalid User Info Response login.oauth2InvalidUserInfoResponse=Invalid User Info Response
login.oauth2invalidRequest=Invalid Request login.oauth2invalidRequest=Invalid Request
login.oauth2AccessDenied=Access Denied login.oauth2AccessDenied=Access Denied
login.oauth2InvalidTokenResponse=Invalid Token Response login.oauth2InvalidTokenResponse=Invalid Token Response
login.oauth2InvalidIdToken=Invalid Id Token login.oauth2InvalidIdToken=Invalid Id Token
login.userIsDisabled=User is deactivated, login is currently blocked with this username. Please contact the administrator.
#auto-redact #auto-redact
@@ -945,6 +961,7 @@ watermark.selectText.6=heightSpacer (مسافة بين كل علامة مائي
watermark.selectText.7=التعتيم (0٪ - 100٪): watermark.selectText.7=التعتيم (0٪ - 100٪):
watermark.selectText.8=Watermark Type: watermark.selectText.8=Watermark Type:
watermark.selectText.9=Watermark Image: watermark.selectText.9=Watermark Image:
watermark.selectText.10=Convert PDF to PDF-Image
watermark.submit=إضافة علامة مائية watermark.submit=إضافة علامة مائية
watermark.type.1=نص watermark.type.1=نص
watermark.type.2=صورة watermark.type.2=صورة
@@ -1125,3 +1142,9 @@ error.copyStack=Copy Stack Trace
error.githubSubmit=GitHub - Submit a ticket error.githubSubmit=GitHub - Submit a ticket
error.discordSubmit=Discord - Submit Support post error.discordSubmit=Discord - Submit Support post
#remove-image
removeImage.title=Remove image
removeImage.header=Remove image
removeImage.removeImage=Remove image
removeImage.submit=Remove image

View File

@@ -55,10 +55,12 @@ userNotFoundMessage=Потребителят не е намерен
incorrectPasswordMessage=Текущата парола е неправилна. incorrectPasswordMessage=Текущата парола е неправилна.
usernameExistsMessage=Новият потребител вече съществува. usernameExistsMessage=Новият потребител вече съществува.
invalidUsernameMessage=Невалидно потребителско име, потребителското име може да съдържа само букви, цифри и следните специални знаци @._+- или трябва да е валиден имейл адрес. invalidUsernameMessage=Невалидно потребителско име, потребителското име може да съдържа само букви, цифри и следните специални знаци @._+- или трябва да е валиден имейл адрес.
invalidPasswordMessage=The password must not be empty and must not have spaces at the beginning or end.
confirmPasswordErrorMessage=New Password and Confirm New Password must match. confirmPasswordErrorMessage=New Password and Confirm New Password must match.
deleteCurrentUserMessage=Не може да се изтрие вписания в момента потребител. deleteCurrentUserMessage=Не може да се изтрие вписания в момента потребител.
deleteUsernameExistsMessage=Потребителското име не съществува и не може да бъде изтрито. deleteUsernameExistsMessage=Потребителското име не съществува и не може да бъде изтрито.
downgradeCurrentUserMessage=Не може да се понижи ролята на текущия потребител downgradeCurrentUserMessage=Не може да се понижи ролята на текущия потребител
disabledCurrentUserMessage=The current user cannot be disabled
downgradeCurrentUserLongMessage=Не може да се понижи ролята на текущия потребител. Следователно текущият потребител няма да бъде показан. downgradeCurrentUserLongMessage=Не може да се понижи ролята на текущия потребител. Следователно текущият потребител няма да бъде показан.
userAlreadyExistsOAuthMessage=Потребителят вече съществува като OAuth2 потребител. userAlreadyExistsOAuthMessage=Потребителят вече съществува като OAuth2 потребител.
userAlreadyExistsWebMessage=Потребителят вече съществува като уеб-потребител. userAlreadyExistsWebMessage=Потребителят вече съществува като уеб-потребител.
@@ -177,6 +179,7 @@ adminUserSettings.user=Потребител
adminUserSettings.addUser=Добавяне на нов потребител adminUserSettings.addUser=Добавяне на нов потребител
adminUserSettings.deleteUser=Delete User adminUserSettings.deleteUser=Delete User
adminUserSettings.confirmDeleteUser=Should the user be deleted? adminUserSettings.confirmDeleteUser=Should the user be deleted?
adminUserSettings.confirmChangeUserStatus=Should the user be disabled/enabled?
adminUserSettings.usernameInfo=Потребителското име може да съдържа само букви, цифри и следните специални символи @._+- или трябва да е валиден имейл адрес. adminUserSettings.usernameInfo=Потребителското име може да съдържа само букви, цифри и следните специални символи @._+- или трябва да е валиден имейл адрес.
adminUserSettings.roles=Роли adminUserSettings.roles=Роли
adminUserSettings.role=Роля adminUserSettings.role=Роля
@@ -190,6 +193,13 @@ adminUserSettings.forceChange=Принудете потребителя да п
adminUserSettings.submit=Съхранете потребителя adminUserSettings.submit=Съхранете потребителя
adminUserSettings.changeUserRole=Промяна на ролята на потребителя adminUserSettings.changeUserRole=Промяна на ролята на потребителя
adminUserSettings.authenticated=Удостоверен adminUserSettings.authenticated=Удостоверен
adminUserSettings.editOwnProfil=Edit own profile
adminUserSettings.enabledUser=enabled user
adminUserSettings.disabledUser=disabled user
adminUserSettings.activeUsers=Active Users:
adminUserSettings.disabledUsers=Disabled Users:
adminUserSettings.totalUsers=Total Users:
adminUserSettings.lastRequest=Last Request
database.title=Database Import/Export database.title=Database Import/Export
@@ -461,6 +471,10 @@ home.BookToPDF.title=Книга към PDF
home.BookToPDF.desc=Преобразува формати на книги/комикси в PDF с помощта на calibre home.BookToPDF.desc=Преобразува формати на книги/комикси в PDF с помощта на calibre
BookToPDF.tags=Книга,комикс,calibre,конвертиране,манга,Amazon,Kindle BookToPDF.tags=Книга,комикс,calibre,конвертиране,манга,Amazon,Kindle
home.removeImagePdf.title=Remove image
home.removeImagePdf.desc=Remove image from PDF to reduce file size
removeImagePdf.tags=Remove Image,Page operations,Back end,server side
########################### ###########################
# # # #
@@ -477,12 +491,14 @@ login.locked=Вашият акаунт е заключен.
login.signinTitle=Моля впишете се login.signinTitle=Моля впишете се
login.ssoSignIn=Влизане чрез еднократно влизане login.ssoSignIn=Влизане чрез еднократно влизане
login.oauth2AutoCreateDisabled=OAUTH2 Автоматично създаване на потребител е деактивирано login.oauth2AutoCreateDisabled=OAUTH2 Автоматично създаване на потребител е деактивирано
login.oauth2AdminBlockedUser=Registration or logging in of non-registered users is currently blocked. Please contact the administrator.
login.oauth2RequestNotFound=Заявката за оторизация не е намерена login.oauth2RequestNotFound=Заявката за оторизация не е намерена
login.oauth2InvalidUserInfoResponse=Невалидна информация за потребителя login.oauth2InvalidUserInfoResponse=Невалидна информация за потребителя
login.oauth2invalidRequest=Невалидна заявка login.oauth2invalidRequest=Невалидна заявка
login.oauth2AccessDenied=Отказан достъп login.oauth2AccessDenied=Отказан достъп
login.oauth2InvalidTokenResponse=Невалиден отговор на токена login.oauth2InvalidTokenResponse=Невалиден отговор на токена
login.oauth2InvalidIdToken=Невалиден токен за идентификатор login.oauth2InvalidIdToken=Невалиден токен за идентификатор
login.userIsDisabled=User is deactivated, login is currently blocked with this username. Please contact the administrator.
#auto-redact #auto-redact
@@ -945,6 +961,7 @@ watermark.selectText.6=дължинаSpacer (Разстояние между в
watermark.selectText.7=Непрозрачност (0% - 100%): watermark.selectText.7=Непрозрачност (0% - 100%):
watermark.selectText.8=Тип воден знак: watermark.selectText.8=Тип воден знак:
watermark.selectText.9=Изображение за воден знак: watermark.selectText.9=Изображение за воден знак:
watermark.selectText.10=Convert PDF to PDF-Image
watermark.submit=Добавяне на воден знак watermark.submit=Добавяне на воден знак
watermark.type.1=Текст watermark.type.1=Текст
watermark.type.2=Изображение watermark.type.2=Изображение
@@ -1125,3 +1142,9 @@ error.copyStack=Копиране на проследяване на стека
error.githubSubmit=GitHub - Изпратете запитване error.githubSubmit=GitHub - Изпратете запитване
error.discordSubmit=Discord - Изпратете запитване за поддръжка error.discordSubmit=Discord - Изпратете запитване за поддръжка
#remove-image
removeImage.title=Remove image
removeImage.header=Remove image
removeImage.removeImage=Remove image
removeImage.submit=Remove image

View File

@@ -55,10 +55,12 @@ userNotFoundMessage=User not found.
incorrectPasswordMessage=Current password is incorrect. incorrectPasswordMessage=Current password is incorrect.
usernameExistsMessage=New Username already exists. usernameExistsMessage=New Username already exists.
invalidUsernameMessage=Invalid username, username can only contain letters, numbers and the following special characters @._+- or must be a valid email address. invalidUsernameMessage=Invalid username, username can only contain letters, numbers and the following special characters @._+- or must be a valid email address.
invalidPasswordMessage=The password must not be empty and must not have spaces at the beginning or end.
confirmPasswordErrorMessage=New Password and Confirm New Password must match. confirmPasswordErrorMessage=New Password and Confirm New Password must match.
deleteCurrentUserMessage=Cannot delete currently logged in user. deleteCurrentUserMessage=Cannot delete currently logged in user.
deleteUsernameExistsMessage=The username does not exist and cannot be deleted. deleteUsernameExistsMessage=The username does not exist and cannot be deleted.
downgradeCurrentUserMessage=No es pot reduir la funció de l'usuari actual downgradeCurrentUserMessage=No es pot reduir la funció de l'usuari actual
disabledCurrentUserMessage=The current user cannot be disabled
downgradeCurrentUserLongMessage=No es pot baixar la funció de l'usuari actual. Per tant, no es mostrarà l'usuari actual. downgradeCurrentUserLongMessage=No es pot baixar la funció de l'usuari actual. Per tant, no es mostrarà l'usuari actual.
userAlreadyExistsOAuthMessage=The user already exists as an OAuth2 user. userAlreadyExistsOAuthMessage=The user already exists as an OAuth2 user.
userAlreadyExistsWebMessage=The user already exists as an web user. userAlreadyExistsWebMessage=The user already exists as an web user.
@@ -177,6 +179,7 @@ adminUserSettings.user=Usuari
adminUserSettings.addUser=Afegir Usuari adminUserSettings.addUser=Afegir Usuari
adminUserSettings.deleteUser=Delete User adminUserSettings.deleteUser=Delete User
adminUserSettings.confirmDeleteUser=Should the user be deleted? adminUserSettings.confirmDeleteUser=Should the user be deleted?
adminUserSettings.confirmChangeUserStatus=Should the user be disabled/enabled?
adminUserSettings.usernameInfo=Username can only contain letters, numbers and the following special characters @._+- or must be a valid email address. adminUserSettings.usernameInfo=Username can only contain letters, numbers and the following special characters @._+- or must be a valid email address.
adminUserSettings.roles=Rols adminUserSettings.roles=Rols
adminUserSettings.role=Rol adminUserSettings.role=Rol
@@ -190,6 +193,13 @@ adminUserSettings.forceChange=Force user to change password on login
adminUserSettings.submit=Desar Usuari adminUserSettings.submit=Desar Usuari
adminUserSettings.changeUserRole=Canvia el rol de l'usuari adminUserSettings.changeUserRole=Canvia el rol de l'usuari
adminUserSettings.authenticated=Authenticated adminUserSettings.authenticated=Authenticated
adminUserSettings.editOwnProfil=Edit own profile
adminUserSettings.enabledUser=enabled user
adminUserSettings.disabledUser=disabled user
adminUserSettings.activeUsers=Active Users:
adminUserSettings.disabledUsers=Disabled Users:
adminUserSettings.totalUsers=Total Users:
adminUserSettings.lastRequest=Last Request
database.title=Database Import/Export database.title=Database Import/Export
@@ -461,6 +471,10 @@ home.BookToPDF.title=Book to PDF
home.BookToPDF.desc=Converts Books/Comics formats to PDF using calibre home.BookToPDF.desc=Converts Books/Comics formats to PDF using calibre
BookToPDF.tags=Book,Comic,Calibre,Convert,manga,amazon,kindle BookToPDF.tags=Book,Comic,Calibre,Convert,manga,amazon,kindle
home.removeImagePdf.title=Remove image
home.removeImagePdf.desc=Remove image from PDF to reduce file size
removeImagePdf.tags=Remove Image,Page operations,Back end,server side
########################### ###########################
# # # #
@@ -477,12 +491,14 @@ login.locked=Compte bloquejat
login.signinTitle=Autenticat login.signinTitle=Autenticat
login.ssoSignIn=Inicia sessió mitjançant l'inici de sessió ún login.ssoSignIn=Inicia sessió mitjançant l'inici de sessió ún
login.oauth2AutoCreateDisabled=L'usuari de creació automàtica OAUTH2 està desactivat login.oauth2AutoCreateDisabled=L'usuari de creació automàtica OAUTH2 està desactivat
login.oauth2AdminBlockedUser=Registration or logging in of non-registered users is currently blocked. Please contact the administrator.
login.oauth2RequestNotFound=Authorization request not found login.oauth2RequestNotFound=Authorization request not found
login.oauth2InvalidUserInfoResponse=Invalid User Info Response login.oauth2InvalidUserInfoResponse=Invalid User Info Response
login.oauth2invalidRequest=Invalid Request login.oauth2invalidRequest=Invalid Request
login.oauth2AccessDenied=Access Denied login.oauth2AccessDenied=Access Denied
login.oauth2InvalidTokenResponse=Invalid Token Response login.oauth2InvalidTokenResponse=Invalid Token Response
login.oauth2InvalidIdToken=Invalid Id Token login.oauth2InvalidIdToken=Invalid Id Token
login.userIsDisabled=User is deactivated, login is currently blocked with this username. Please contact the administrator.
#auto-redact #auto-redact
@@ -945,6 +961,7 @@ watermark.selectText.6=separació d'alçada (Espai vertical entre cada Marca d'A
watermark.selectText.7=Opacitat (0% - 100%): watermark.selectText.7=Opacitat (0% - 100%):
watermark.selectText.8=Watermark Type: watermark.selectText.8=Watermark Type:
watermark.selectText.9=Watermark Image: watermark.selectText.9=Watermark Image:
watermark.selectText.10=Convert PDF to PDF-Image
watermark.submit=Afegir Marca d'Aigua watermark.submit=Afegir Marca d'Aigua
watermark.type.1=Text watermark.type.1=Text
watermark.type.2=Image watermark.type.2=Image
@@ -1125,3 +1142,9 @@ error.copyStack=Copy Stack Trace
error.githubSubmit=GitHub - Submit a ticket error.githubSubmit=GitHub - Submit a ticket
error.discordSubmit=Discord - Submit Support post error.discordSubmit=Discord - Submit Support post
#remove-image
removeImage.title=Remove image
removeImage.header=Remove image
removeImage.removeImage=Remove image
removeImage.submit=Remove image

View File

@@ -55,10 +55,12 @@ userNotFoundMessage=Uživatel nenalezen.
incorrectPasswordMessage=Současné heslo není správné. incorrectPasswordMessage=Současné heslo není správné.
usernameExistsMessage=Nové uživatelské jméno již existuje. usernameExistsMessage=Nové uživatelské jméno již existuje.
invalidUsernameMessage=Nesprávné uživatelské jméno, smí obsahovat pouze písmena, číslice a následující speciální znaky @._+- nebo musí být validní emailová adresa. invalidUsernameMessage=Nesprávné uživatelské jméno, smí obsahovat pouze písmena, číslice a následující speciální znaky @._+- nebo musí být validní emailová adresa.
invalidPasswordMessage=The password must not be empty and must not have spaces at the beginning or end.
confirmPasswordErrorMessage=New Password and Confirm New Password must match. confirmPasswordErrorMessage=New Password and Confirm New Password must match.
deleteCurrentUserMessage=Nelze smazat aktuální přihlášeného uživatele. deleteCurrentUserMessage=Nelze smazat aktuální přihlášeného uživatele.
deleteUsernameExistsMessage=Uživatelské jméno neexistuje a nelze ho smazat. deleteUsernameExistsMessage=Uživatelské jméno neexistuje a nelze ho smazat.
downgradeCurrentUserMessage=Nelze snížit roli aktuálního uživatele. downgradeCurrentUserMessage=Nelze snížit roli aktuálního uživatele.
disabledCurrentUserMessage=The current user cannot be disabled
downgradeCurrentUserLongMessage=Nelze snížit roli aktuálního uživatele. Proto nebude aktuální uživatel zobrazen. downgradeCurrentUserLongMessage=Nelze snížit roli aktuálního uživatele. Proto nebude aktuální uživatel zobrazen.
userAlreadyExistsOAuthMessage=Uživatel již existuje jako OAuth2 uživatel. userAlreadyExistsOAuthMessage=Uživatel již existuje jako OAuth2 uživatel.
userAlreadyExistsWebMessage=Uživatel již existuje jako webový uživatel. userAlreadyExistsWebMessage=Uživatel již existuje jako webový uživatel.
@@ -177,6 +179,7 @@ adminUserSettings.user=Uživatel
adminUserSettings.addUser=Přidat Nového Uživatele adminUserSettings.addUser=Přidat Nového Uživatele
adminUserSettings.deleteUser=Delete User adminUserSettings.deleteUser=Delete User
adminUserSettings.confirmDeleteUser=Should the user be deleted? adminUserSettings.confirmDeleteUser=Should the user be deleted?
adminUserSettings.confirmChangeUserStatus=Should the user be disabled/enabled?
adminUserSettings.usernameInfo=Uživatelské Jméno může obsahovat pouze písmena, čísla a následující speciální znaky @._+- nebo musí být správná emailová adresa. adminUserSettings.usernameInfo=Uživatelské Jméno může obsahovat pouze písmena, čísla a následující speciální znaky @._+- nebo musí být správná emailová adresa.
adminUserSettings.roles=Role adminUserSettings.roles=Role
adminUserSettings.role=Role adminUserSettings.role=Role
@@ -190,6 +193,13 @@ adminUserSettings.forceChange=Vynutit uživateli změnu hesla při přihlášen
adminUserSettings.submit=Uložit Uživatele adminUserSettings.submit=Uložit Uživatele
adminUserSettings.changeUserRole=Zmenit Roli Uživatele adminUserSettings.changeUserRole=Zmenit Roli Uživatele
adminUserSettings.authenticated=Ověřeno adminUserSettings.authenticated=Ověřeno
adminUserSettings.editOwnProfil=Edit own profile
adminUserSettings.enabledUser=enabled user
adminUserSettings.disabledUser=disabled user
adminUserSettings.activeUsers=Active Users:
adminUserSettings.disabledUsers=Disabled Users:
adminUserSettings.totalUsers=Total Users:
adminUserSettings.lastRequest=Last Request
database.title=Database Import/Export database.title=Database Import/Export
@@ -461,6 +471,10 @@ home.BookToPDF.title=Kniha na PDF
home.BookToPDF.desc=Převádí formáty knih/komiksů do PDF pomocí calibre home.BookToPDF.desc=Převádí formáty knih/komiksů do PDF pomocí calibre
BookToPDF.tags=Kniha,Komiks,Calibre,Konvertovat,manga,amazon,kindle,epub,mobi,azw3,docx,rtf,txt,html,lit,fb2,pdb,lrf BookToPDF.tags=Kniha,Komiks,Calibre,Konvertovat,manga,amazon,kindle,epub,mobi,azw3,docx,rtf,txt,html,lit,fb2,pdb,lrf
home.removeImagePdf.title=Remove image
home.removeImagePdf.desc=Remove image from PDF to reduce file size
removeImagePdf.tags=Remove Image,Page operations,Back end,server side
########################### ###########################
# # # #
@@ -477,12 +491,14 @@ login.locked=Your account has been locked.
login.signinTitle=Please sign in login.signinTitle=Please sign in
login.ssoSignIn=Login via Single Sign-on login.ssoSignIn=Login via Single Sign-on
login.oauth2AutoCreateDisabled=OAUTH2 Auto-Create User Disabled login.oauth2AutoCreateDisabled=OAUTH2 Auto-Create User Disabled
login.oauth2AdminBlockedUser=Registration or logging in of non-registered users is currently blocked. Please contact the administrator.
login.oauth2RequestNotFound=Authorization request not found login.oauth2RequestNotFound=Authorization request not found
login.oauth2InvalidUserInfoResponse=Invalid User Info Response login.oauth2InvalidUserInfoResponse=Invalid User Info Response
login.oauth2invalidRequest=Invalid Request login.oauth2invalidRequest=Invalid Request
login.oauth2AccessDenied=Access Denied login.oauth2AccessDenied=Access Denied
login.oauth2InvalidTokenResponse=Invalid Token Response login.oauth2InvalidTokenResponse=Invalid Token Response
login.oauth2InvalidIdToken=Invalid Id Token login.oauth2InvalidIdToken=Invalid Id Token
login.userIsDisabled=User is deactivated, login is currently blocked with this username. Please contact the administrator.
#auto-redact #auto-redact
@@ -945,6 +961,7 @@ watermark.selectText.6=Výška mezery (Mezera mezi každým vodoznakem svisle):
watermark.selectText.7=Průhlednost (0% - 100%): watermark.selectText.7=Průhlednost (0% - 100%):
watermark.selectText.8=Typ vodoznaku: watermark.selectText.8=Typ vodoznaku:
watermark.selectText.9=Obrázek vodoznaku: watermark.selectText.9=Obrázek vodoznaku:
watermark.selectText.10=Convert PDF to PDF-Image
watermark.submit=Přidat vodoznak watermark.submit=Přidat vodoznak
watermark.type.1=Text watermark.type.1=Text
watermark.type.2=Obrázek watermark.type.2=Obrázek
@@ -1125,3 +1142,9 @@ error.copyStack=Kopírovat stopu zásobníku
error.githubSubmit=GitHub - Odeslat požadavek error.githubSubmit=GitHub - Odeslat požadavek
error.discordSubmit=Discord - Odeslat příspěvek podpory error.discordSubmit=Discord - Odeslat příspěvek podpory
#remove-image
removeImage.title=Remove image
removeImage.header=Remove image
removeImage.removeImage=Remove image
removeImage.submit=Remove image

File diff suppressed because it is too large Load Diff

View File

@@ -55,10 +55,12 @@ userNotFoundMessage=Benutzer nicht gefunden.
incorrectPasswordMessage=Das Passwort ist falsch. incorrectPasswordMessage=Das Passwort ist falsch.
usernameExistsMessage=Neuer Benutzername existiert bereits. usernameExistsMessage=Neuer Benutzername existiert bereits.
invalidUsernameMessage=Ungültiger Benutzername. Der Benutzername darf nur Buchstaben, Zahlen und die folgenden Sonderzeichen @._+- enthalten oder muss eine gültige E-Mail-Adresse sein. invalidUsernameMessage=Ungültiger Benutzername. Der Benutzername darf nur Buchstaben, Zahlen und die folgenden Sonderzeichen @._+- enthalten oder muss eine gültige E-Mail-Adresse sein.
invalidPasswordMessage=Das Passwort darf nicht leer sein und kein Leerzeichen am Anfang und Ende haben.
confirmPasswordErrorMessage=„Neues Passwort“ und „Neues Passwort bestätigen“ müssen übereinstimmen. confirmPasswordErrorMessage=„Neues Passwort“ und „Neues Passwort bestätigen“ müssen übereinstimmen.
deleteCurrentUserMessage=Der aktuell angemeldete Benutzer kann nicht gelöscht werden. deleteCurrentUserMessage=Der aktuell angemeldete Benutzer kann nicht gelöscht werden.
deleteUsernameExistsMessage=Der Benutzername existiert nicht und kann nicht gelöscht werden. deleteUsernameExistsMessage=Der Benutzername existiert nicht und kann nicht gelöscht werden.
downgradeCurrentUserMessage=Die Rolle des aktuellen Benutzers kann nicht herabgestuft werden downgradeCurrentUserMessage=Die Rolle des aktuellen Benutzers kann nicht herabgestuft werden
disabledCurrentUserMessage=Der aktuelle Benutzer kann nicht deaktiviert werden
downgradeCurrentUserLongMessage=Die Rolle des aktuellen Benutzers kann nicht herabgestuft werden. Daher wird der aktuelle Benutzer nicht angezeigt. downgradeCurrentUserLongMessage=Die Rolle des aktuellen Benutzers kann nicht herabgestuft werden. Daher wird der aktuelle Benutzer nicht angezeigt.
userAlreadyExistsOAuthMessage=Der Benutzer ist bereits als OAuth2-Benutzer vorhanden. userAlreadyExistsOAuthMessage=Der Benutzer ist bereits als OAuth2-Benutzer vorhanden.
userAlreadyExistsWebMessage=Der Benutzer ist bereits als Webbenutzer vorhanden. userAlreadyExistsWebMessage=Der Benutzer ist bereits als Webbenutzer vorhanden.
@@ -177,10 +179,11 @@ adminUserSettings.user=Benutzer
adminUserSettings.addUser=Neuen Benutzer hinzufügen adminUserSettings.addUser=Neuen Benutzer hinzufügen
adminUserSettings.deleteUser=Benutzer löschen adminUserSettings.deleteUser=Benutzer löschen
adminUserSettings.confirmDeleteUser=Soll der Benutzer gelöscht werden? adminUserSettings.confirmDeleteUser=Soll der Benutzer gelöscht werden?
adminUserSettings.confirmChangeUserStatus=Should the user be disabled/enabled?
adminUserSettings.usernameInfo=Der Benutzername darf nur Buchstaben, Zahlen und die folgenden Sonderzeichen @._+- enthalten oder muss eine gültige E-Mail-Adresse sein. adminUserSettings.usernameInfo=Der Benutzername darf nur Buchstaben, Zahlen und die folgenden Sonderzeichen @._+- enthalten oder muss eine gültige E-Mail-Adresse sein.
adminUserSettings.roles=Rollen adminUserSettings.roles=Rollen
adminUserSettings.role=Rolle adminUserSettings.role=Rolle
adminUserSettings.actions=Aktion adminUserSettings.actions=Aktions
adminUserSettings.apiUser=Eingeschränkter API-Benutzer adminUserSettings.apiUser=Eingeschränkter API-Benutzer
adminUserSettings.extraApiUser=Zusätzlicher eingeschränkter API-Benutzer adminUserSettings.extraApiUser=Zusätzlicher eingeschränkter API-Benutzer
adminUserSettings.webOnlyUser=Nur Web-Benutzer adminUserSettings.webOnlyUser=Nur Web-Benutzer
@@ -190,6 +193,13 @@ adminUserSettings.forceChange=Benutzer dazu zwingen, Benutzernamen/Passwort bei
adminUserSettings.submit=Benutzer speichern adminUserSettings.submit=Benutzer speichern
adminUserSettings.changeUserRole=Benutzerrolle ändern adminUserSettings.changeUserRole=Benutzerrolle ändern
adminUserSettings.authenticated=Authentifiziert adminUserSettings.authenticated=Authentifiziert
adminUserSettings.editOwnProfil=Eigenes Profil bearbeiten
adminUserSettings.enabledUser=enabled user
adminUserSettings.disabledUser=disabled user
adminUserSettings.activeUsers=Active Users:
adminUserSettings.disabledUsers=Disabled Users:
adminUserSettings.totalUsers=Total Users:
adminUserSettings.lastRequest=Last Request
database.title=Datenbank Import/Export database.title=Datenbank Import/Export
@@ -461,6 +471,10 @@ home.BookToPDF.title=Buch als PDF
home.BookToPDF.desc=Konvertiert Buch-/Comic-Formate mithilfe von Calibre in PDF home.BookToPDF.desc=Konvertiert Buch-/Comic-Formate mithilfe von Calibre in PDF
BookToPDF.tags=buch,comic,calibre,convert,manga,amazon,kindle BookToPDF.tags=buch,comic,calibre,convert,manga,amazon,kindle
home.removeImagePdf.title=Remove image
home.removeImagePdf.desc=Remove image from PDF to reduce file size
removeImagePdf.tags=Remove Image,Page operations,Back end,server side
########################### ###########################
# # # #
@@ -477,12 +491,14 @@ login.locked=Ihr Konto wurde gesperrt.
login.signinTitle=Bitte melden Sie sich an. login.signinTitle=Bitte melden Sie sich an.
login.ssoSignIn=Anmeldung per Single Sign-On login.ssoSignIn=Anmeldung per Single Sign-On
login.oauth2AutoCreateDisabled=OAUTH2 Benutzer automatisch erstellen deaktiviert login.oauth2AutoCreateDisabled=OAUTH2 Benutzer automatisch erstellen deaktiviert
login.oauth2AdminBlockedUser=Die Registrierung bzw. das anmelden von nicht registrierten Benutzern ist derzeit gesperrt. Bitte wenden Sie sich an den Administrator.
login.oauth2RequestNotFound=Autorisierungsanfrage nicht gefunden login.oauth2RequestNotFound=Autorisierungsanfrage nicht gefunden
login.oauth2InvalidUserInfoResponse=Ungültige Benutzerinformationsantwort login.oauth2InvalidUserInfoResponse=Ungültige Benutzerinformationsantwort
login.oauth2invalidRequest=ungültige Anfrage login.oauth2invalidRequest=ungültige Anfrage
login.oauth2AccessDenied=Zugriff abgelehnt login.oauth2AccessDenied=Zugriff abgelehnt
login.oauth2InvalidTokenResponse=Ungültige Token-Antwort login.oauth2InvalidTokenResponse=Ungültige Token-Antwort
login.oauth2InvalidIdToken=Ungültiges ID-Token login.oauth2InvalidIdToken=Ungültiges ID-Token
login.userIsDisabled=Benutzer ist deaktiviert, die Anmeldung ist mit diesem Benutzernamen derzeit gesperrt. Bitte wenden Sie sich an den Administrator.
#auto-redact #auto-redact
@@ -945,6 +961,7 @@ watermark.selectText.6=höheSpacer (vertikaler Abstand zwischen den einzelnen Wa
watermark.selectText.7=Deckkraft (0% - 100 %): watermark.selectText.7=Deckkraft (0% - 100 %):
watermark.selectText.8=Wasserzeichen Typ: watermark.selectText.8=Wasserzeichen Typ:
watermark.selectText.9=Wasserzeichen-Bild: watermark.selectText.9=Wasserzeichen-Bild:
watermark.selectText.10=Convert PDF to PDF-Image
watermark.submit=Wasserzeichen hinzufügen watermark.submit=Wasserzeichen hinzufügen
watermark.type.1=Text watermark.type.1=Text
watermark.type.2=Bild watermark.type.2=Bild
@@ -1125,3 +1142,9 @@ error.copyStack=Stack-Trace kopieren
error.githubSubmit=GitHub - Ein Ticket einreichen error.githubSubmit=GitHub - Ein Ticket einreichen
error.discordSubmit=Discord - Unterstützungsbeitrag einreichen error.discordSubmit=Discord - Unterstützungsbeitrag einreichen
#remove-image
removeImage.title=Remove image
removeImage.header=Remove image
removeImage.removeImage=Remove image
removeImage.submit=Remove image

View File

@@ -55,10 +55,12 @@ userNotFoundMessage=Ο χρήστης δεν βρέθηκε.
incorrectPasswordMessage=Ο τρέχων κωδικός πρόσβασης είναι λανθασμένος. incorrectPasswordMessage=Ο τρέχων κωδικός πρόσβασης είναι λανθασμένος.
usernameExistsMessage=Το νέο όνομα χρήστη υπάρχει ήδη. usernameExistsMessage=Το νέο όνομα χρήστη υπάρχει ήδη.
invalidUsernameMessage=Μη έγκυρο όνομα χρήστη, όνομα χρήστη μπορεί να περιέχει μόνο γράμματα, αριθμούς και τους ακόλουθους ειδικούς χαρακτήρες @._+- ή πρέπει να είναι έγκυρη διεύθυνση email. invalidUsernameMessage=Μη έγκυρο όνομα χρήστη, όνομα χρήστη μπορεί να περιέχει μόνο γράμματα, αριθμούς και τους ακόλουθους ειδικούς χαρακτήρες @._+- ή πρέπει να είναι έγκυρη διεύθυνση email.
invalidPasswordMessage=The password must not be empty and must not have spaces at the beginning or end.
confirmPasswordErrorMessage=New Password and Confirm New Password must match. confirmPasswordErrorMessage=New Password and Confirm New Password must match.
deleteCurrentUserMessage=Δεν είναι δυνατή η διαγραφή του τρέχοντος συνδεδεμένου χρήστη. deleteCurrentUserMessage=Δεν είναι δυνατή η διαγραφή του τρέχοντος συνδεδεμένου χρήστη.
deleteUsernameExistsMessage=Το όνομα χρήστη δεν υπάρχει και δεν μπορεί να διαγραφεί. deleteUsernameExistsMessage=Το όνομα χρήστη δεν υπάρχει και δεν μπορεί να διαγραφεί.
downgradeCurrentUserMessage=Δεν είναι δυνατή η υποβάθμιση του ρόλου του τρέχοντος χρήστη downgradeCurrentUserMessage=Δεν είναι δυνατή η υποβάθμιση του ρόλου του τρέχοντος χρήστη
disabledCurrentUserMessage=The current user cannot be disabled
downgradeCurrentUserLongMessage=Δεν είναι δυνατή η υποβάθμιση του ρόλου του τρέχοντος χρήστη. Ως εκ τούτου, ο τρέχων χρήστης δεν θα εμφανίζεται. downgradeCurrentUserLongMessage=Δεν είναι δυνατή η υποβάθμιση του ρόλου του τρέχοντος χρήστη. Ως εκ τούτου, ο τρέχων χρήστης δεν θα εμφανίζεται.
userAlreadyExistsOAuthMessage=The user already exists as an OAuth2 user. userAlreadyExistsOAuthMessage=The user already exists as an OAuth2 user.
userAlreadyExistsWebMessage=The user already exists as an web user. userAlreadyExistsWebMessage=The user already exists as an web user.
@@ -177,6 +179,7 @@ adminUserSettings.user=Χρήστης
adminUserSettings.addUser=Προσθήκη νέου Χρήστη adminUserSettings.addUser=Προσθήκη νέου Χρήστη
adminUserSettings.deleteUser=Delete User adminUserSettings.deleteUser=Delete User
adminUserSettings.confirmDeleteUser=Should the user be deleted? adminUserSettings.confirmDeleteUser=Should the user be deleted?
adminUserSettings.confirmChangeUserStatus=Should the user be disabled/enabled?
adminUserSettings.usernameInfo=Username can only contain letters, numbers and the following special characters @._+- or must be a valid email address. adminUserSettings.usernameInfo=Username can only contain letters, numbers and the following special characters @._+- or must be a valid email address.
adminUserSettings.roles=Ρόλοι adminUserSettings.roles=Ρόλοι
adminUserSettings.role=Ρόλος adminUserSettings.role=Ρόλος
@@ -190,6 +193,13 @@ adminUserSettings.forceChange=Αναγκάστε τον χρήστη να αλλ
adminUserSettings.submit=Αποθήκευση Χρήστη adminUserSettings.submit=Αποθήκευση Χρήστη
adminUserSettings.changeUserRole=Αλλαγή ρόλου χρήστη adminUserSettings.changeUserRole=Αλλαγή ρόλου χρήστη
adminUserSettings.authenticated=Authenticated adminUserSettings.authenticated=Authenticated
adminUserSettings.editOwnProfil=Edit own profile
adminUserSettings.enabledUser=enabled user
adminUserSettings.disabledUser=disabled user
adminUserSettings.activeUsers=Active Users:
adminUserSettings.disabledUsers=Disabled Users:
adminUserSettings.totalUsers=Total Users:
adminUserSettings.lastRequest=Last Request
database.title=Database Import/Export database.title=Database Import/Export
@@ -461,6 +471,10 @@ home.BookToPDF.title=Book σε PDF
home.BookToPDF.desc=Μετατρέπει τις μορφές Books/Comics σε PDF χρησιμοποιώντας calibre home.BookToPDF.desc=Μετατρέπει τις μορφές Books/Comics σε PDF χρησιμοποιώντας calibre
BookToPDF.tags=Book,Comic,Calibre,Convert,manga,amazon,kindle BookToPDF.tags=Book,Comic,Calibre,Convert,manga,amazon,kindle
home.removeImagePdf.title=Remove image
home.removeImagePdf.desc=Remove image from PDF to reduce file size
removeImagePdf.tags=Remove Image,Page operations,Back end,server side
########################### ###########################
# # # #
@@ -477,12 +491,14 @@ login.locked=Ο λογαριασμός σας έχει κλειδωθεί.
login.signinTitle=Παρακαλώ, συνδεθείτε login.signinTitle=Παρακαλώ, συνδεθείτε
login.ssoSignIn=Σύνδεση μέσω μοναδικής σύνδεσης login.ssoSignIn=Σύνδεση μέσω μοναδικής σύνδεσης
login.oauth2AutoCreateDisabled=Απενεργοποιήθηκε ο χρήστης αυτόματης δημιουργίας OAUTH2 login.oauth2AutoCreateDisabled=Απενεργοποιήθηκε ο χρήστης αυτόματης δημιουργίας OAUTH2
login.oauth2AdminBlockedUser=Registration or logging in of non-registered users is currently blocked. Please contact the administrator.
login.oauth2RequestNotFound=Authorization request not found login.oauth2RequestNotFound=Authorization request not found
login.oauth2InvalidUserInfoResponse=Invalid User Info Response login.oauth2InvalidUserInfoResponse=Invalid User Info Response
login.oauth2invalidRequest=Invalid Request login.oauth2invalidRequest=Invalid Request
login.oauth2AccessDenied=Access Denied login.oauth2AccessDenied=Access Denied
login.oauth2InvalidTokenResponse=Invalid Token Response login.oauth2InvalidTokenResponse=Invalid Token Response
login.oauth2InvalidIdToken=Invalid Id Token login.oauth2InvalidIdToken=Invalid Id Token
login.userIsDisabled=User is deactivated, login is currently blocked with this username. Please contact the administrator.
#auto-redact #auto-redact
@@ -945,6 +961,7 @@ watermark.selectText.6=heightSpacer (Κενό μεταξύ κάθε υδατογ
watermark.selectText.7=Αδιαφάνεια (Opacity) (0% - 100%): watermark.selectText.7=Αδιαφάνεια (Opacity) (0% - 100%):
watermark.selectText.8=Τύπος Υδατογραφήματος: watermark.selectText.8=Τύπος Υδατογραφήματος:
watermark.selectText.9=Εικόνα Υδατογραφήματος: watermark.selectText.9=Εικόνα Υδατογραφήματος:
watermark.selectText.10=Convert PDF to PDF-Image
watermark.submit=Προσθήκη Υδατογραφήματος watermark.submit=Προσθήκη Υδατογραφήματος
watermark.type.1=Κείμενο watermark.type.1=Κείμενο
watermark.type.2=Εικόνα watermark.type.2=Εικόνα
@@ -1125,3 +1142,9 @@ error.copyStack=Αντιγραφή Stack Trace
error.githubSubmit=GitHub - Υποβάλετε ένα ticket error.githubSubmit=GitHub - Υποβάλετε ένα ticket
error.discordSubmit=Discord - Υποβάλετε ένα Support post error.discordSubmit=Discord - Υποβάλετε ένα Support post
#remove-image
removeImage.title=Remove image
removeImage.header=Remove image
removeImage.removeImage=Remove image
removeImage.submit=Remove image

View File

@@ -55,10 +55,12 @@ userNotFoundMessage=User not found.
incorrectPasswordMessage=Current password is incorrect. incorrectPasswordMessage=Current password is incorrect.
usernameExistsMessage=New Username already exists. usernameExistsMessage=New Username already exists.
invalidUsernameMessage=Invalid username, username can only contain letters, numbers and the following special characters @._+- or must be a valid email address. invalidUsernameMessage=Invalid username, username can only contain letters, numbers and the following special characters @._+- or must be a valid email address.
invalidPasswordMessage=The password must not be empty and must not have spaces at the beginning or end.
confirmPasswordErrorMessage=New Password and Confirm New Password must match. confirmPasswordErrorMessage=New Password and Confirm New Password must match.
deleteCurrentUserMessage=Cannot delete currently logged in user. deleteCurrentUserMessage=Cannot delete currently logged in user.
deleteUsernameExistsMessage=The username does not exist and cannot be deleted. deleteUsernameExistsMessage=The username does not exist and cannot be deleted.
downgradeCurrentUserMessage=Cannot downgrade current user's role downgradeCurrentUserMessage=Cannot downgrade current user's role
disabledCurrentUserMessage=The current user cannot be disabled
downgradeCurrentUserLongMessage=Cannot downgrade current user's role. Hence, current user will not be shown. downgradeCurrentUserLongMessage=Cannot downgrade current user's role. Hence, current user will not be shown.
userAlreadyExistsOAuthMessage=The user already exists as an OAuth2 user. userAlreadyExistsOAuthMessage=The user already exists as an OAuth2 user.
userAlreadyExistsWebMessage=The user already exists as an web user. userAlreadyExistsWebMessage=The user already exists as an web user.
@@ -177,6 +179,7 @@ adminUserSettings.user=User
adminUserSettings.addUser=Add New User adminUserSettings.addUser=Add New User
adminUserSettings.deleteUser=Delete User adminUserSettings.deleteUser=Delete User
adminUserSettings.confirmDeleteUser=Should the user be deleted? adminUserSettings.confirmDeleteUser=Should the user be deleted?
adminUserSettings.confirmChangeUserStatus=Should the user be disabled/enabled?
adminUserSettings.usernameInfo=Username can only contain letters, numbers and the following special characters @._+- or must be a valid email address. adminUserSettings.usernameInfo=Username can only contain letters, numbers and the following special characters @._+- or must be a valid email address.
adminUserSettings.roles=Roles adminUserSettings.roles=Roles
adminUserSettings.role=Role adminUserSettings.role=Role
@@ -190,6 +193,13 @@ adminUserSettings.forceChange=Force user to change password on login
adminUserSettings.submit=Save User adminUserSettings.submit=Save User
adminUserSettings.changeUserRole=Change User's Role adminUserSettings.changeUserRole=Change User's Role
adminUserSettings.authenticated=Authenticated adminUserSettings.authenticated=Authenticated
adminUserSettings.editOwnProfil=Edit own profile
adminUserSettings.enabledUser=enabled user
adminUserSettings.disabledUser=disabled user
adminUserSettings.activeUsers=Active Users:
adminUserSettings.disabledUsers=Disabled Users:
adminUserSettings.totalUsers=Total Users:
adminUserSettings.lastRequest=Last Request
database.title=Database Import/Export database.title=Database Import/Export
@@ -461,6 +471,10 @@ home.BookToPDF.title=Book to PDF
home.BookToPDF.desc=Converts Books/Comics formats to PDF using calibre home.BookToPDF.desc=Converts Books/Comics formats to PDF using calibre
BookToPDF.tags=Book,Comic,Calibre,Convert,manga,amazon,kindle,epub,mobi,azw3,docx,rtf,txt,html,lit,fb2,pdb,lrf BookToPDF.tags=Book,Comic,Calibre,Convert,manga,amazon,kindle,epub,mobi,azw3,docx,rtf,txt,html,lit,fb2,pdb,lrf
home.removeImagePdf.title=Remove image
home.removeImagePdf.desc=Remove image from PDF to reduce file size
removeImagePdf.tags=Remove Image,Page operations,Back end,server side
########################### ###########################
# # # #
@@ -477,12 +491,14 @@ login.locked=Your account has been locked.
login.signinTitle=Please sign in login.signinTitle=Please sign in
login.ssoSignIn=Login via Single Sign-on login.ssoSignIn=Login via Single Sign-on
login.oauth2AutoCreateDisabled=OAUTH2 Auto-Create User Disabled login.oauth2AutoCreateDisabled=OAUTH2 Auto-Create User Disabled
login.oauth2AdminBlockedUser=Registration or logging in of non-registered users is currently blocked. Please contact the administrator.
login.oauth2RequestNotFound=Authorization request not found login.oauth2RequestNotFound=Authorization request not found
login.oauth2InvalidUserInfoResponse=Invalid User Info Response login.oauth2InvalidUserInfoResponse=Invalid User Info Response
login.oauth2invalidRequest=Invalid Request login.oauth2invalidRequest=Invalid Request
login.oauth2AccessDenied=Access Denied login.oauth2AccessDenied=Access Denied
login.oauth2InvalidTokenResponse=Invalid Token Response login.oauth2InvalidTokenResponse=Invalid Token Response
login.oauth2InvalidIdToken=Invalid Id Token login.oauth2InvalidIdToken=Invalid Id Token
login.userIsDisabled=User is deactivated, login is currently blocked with this username. Please contact the administrator.
#auto-redact #auto-redact
@@ -945,6 +961,7 @@ watermark.selectText.6=heightSpacer (Space between each watermark vertically):
watermark.selectText.7=Opacity (0% - 100%): watermark.selectText.7=Opacity (0% - 100%):
watermark.selectText.8=Watermark Type: watermark.selectText.8=Watermark Type:
watermark.selectText.9=Watermark Image: watermark.selectText.9=Watermark Image:
watermark.selectText.10=Convert PDF to PDF-Image
watermark.submit=Add Watermark watermark.submit=Add Watermark
watermark.type.1=Text watermark.type.1=Text
watermark.type.2=Image watermark.type.2=Image
@@ -1125,3 +1142,9 @@ error.copyStack=Copy Stack Trace
error.githubSubmit=GitHub - Submit a ticket error.githubSubmit=GitHub - Submit a ticket
error.discordSubmit=Discord - Submit Support post error.discordSubmit=Discord - Submit Support post
#remove-image
removeImage.title=Remove image
removeImage.header=Remove image
removeImage.removeImage=Remove image
removeImage.submit=Remove image

View File

@@ -55,10 +55,12 @@ userNotFoundMessage=User not found.
incorrectPasswordMessage=Current password is incorrect. incorrectPasswordMessage=Current password is incorrect.
usernameExistsMessage=New Username already exists. usernameExistsMessage=New Username already exists.
invalidUsernameMessage=Invalid username, username can only contain letters, numbers and the following special characters @._+- or must be a valid email address. invalidUsernameMessage=Invalid username, username can only contain letters, numbers and the following special characters @._+- or must be a valid email address.
invalidPasswordMessage=The password must not be empty and must not have spaces at the beginning or end.
confirmPasswordErrorMessage=New Password and Confirm New Password must match. confirmPasswordErrorMessage=New Password and Confirm New Password must match.
deleteCurrentUserMessage=Cannot delete currently logged in user. deleteCurrentUserMessage=Cannot delete currently logged in user.
deleteUsernameExistsMessage=The username does not exist and cannot be deleted. deleteUsernameExistsMessage=The username does not exist and cannot be deleted.
downgradeCurrentUserMessage=Cannot downgrade current user's role downgradeCurrentUserMessage=Cannot downgrade current user's role
disabledCurrentUserMessage=The current user cannot be disabled
downgradeCurrentUserLongMessage=Cannot downgrade current user's role. Hence, current user will not be shown. downgradeCurrentUserLongMessage=Cannot downgrade current user's role. Hence, current user will not be shown.
userAlreadyExistsOAuthMessage=The user already exists as an OAuth2 user. userAlreadyExistsOAuthMessage=The user already exists as an OAuth2 user.
userAlreadyExistsWebMessage=The user already exists as an web user. userAlreadyExistsWebMessage=The user already exists as an web user.
@@ -177,6 +179,7 @@ adminUserSettings.user=User
adminUserSettings.addUser=Add New User adminUserSettings.addUser=Add New User
adminUserSettings.deleteUser=Delete User adminUserSettings.deleteUser=Delete User
adminUserSettings.confirmDeleteUser=Should the user be deleted? adminUserSettings.confirmDeleteUser=Should the user be deleted?
adminUserSettings.confirmChangeUserStatus=Should the user be disabled/enabled?
adminUserSettings.usernameInfo=Username can only contain letters, numbers and the following special characters @._+- or must be a valid email address. adminUserSettings.usernameInfo=Username can only contain letters, numbers and the following special characters @._+- or must be a valid email address.
adminUserSettings.roles=Roles adminUserSettings.roles=Roles
adminUserSettings.role=Role adminUserSettings.role=Role
@@ -190,6 +193,13 @@ adminUserSettings.forceChange=Force user to change password on login
adminUserSettings.submit=Save User adminUserSettings.submit=Save User
adminUserSettings.changeUserRole=Change User's Role adminUserSettings.changeUserRole=Change User's Role
adminUserSettings.authenticated=Authenticated adminUserSettings.authenticated=Authenticated
adminUserSettings.editOwnProfil=Edit own profile
adminUserSettings.enabledUser=enabled user
adminUserSettings.disabledUser=disabled user
adminUserSettings.activeUsers=Active Users:
adminUserSettings.disabledUsers=Disabled Users:
adminUserSettings.totalUsers=Total Users:
adminUserSettings.lastRequest=Last Request
database.title=Database Import/Export database.title=Database Import/Export
@@ -461,6 +471,10 @@ home.BookToPDF.title=Book to PDF
home.BookToPDF.desc=Converts Books/Comics formats to PDF using calibre home.BookToPDF.desc=Converts Books/Comics formats to PDF using calibre
BookToPDF.tags=Book,Comic,Calibre,Convert,manga,amazon,kindle BookToPDF.tags=Book,Comic,Calibre,Convert,manga,amazon,kindle
home.removeImagePdf.title=Remove image
home.removeImagePdf.desc=Remove image from PDF to reduce file size
removeImagePdf.tags=Remove Image,Page operations,Back end,server side
########################### ###########################
# # # #
@@ -477,12 +491,14 @@ login.locked=Your account has been locked.
login.signinTitle=Please sign in login.signinTitle=Please sign in
login.ssoSignIn=Login via Single Sign-on login.ssoSignIn=Login via Single Sign-on
login.oauth2AutoCreateDisabled=OAUTH2 Auto-Create User Disabled login.oauth2AutoCreateDisabled=OAUTH2 Auto-Create User Disabled
login.oauth2AdminBlockedUser=Registration or logging in of non-registered users is currently blocked. Please contact the administrator.
login.oauth2RequestNotFound=Authorization request not found login.oauth2RequestNotFound=Authorization request not found
login.oauth2InvalidUserInfoResponse=Invalid User Info Response login.oauth2InvalidUserInfoResponse=Invalid User Info Response
login.oauth2invalidRequest=Invalid Request login.oauth2invalidRequest=Invalid Request
login.oauth2AccessDenied=Access Denied login.oauth2AccessDenied=Access Denied
login.oauth2InvalidTokenResponse=Invalid Token Response login.oauth2InvalidTokenResponse=Invalid Token Response
login.oauth2InvalidIdToken=Invalid Id Token login.oauth2InvalidIdToken=Invalid Id Token
login.userIsDisabled=User is deactivated, login is currently blocked with this username. Please contact the administrator.
#auto-redact #auto-redact
@@ -945,6 +961,7 @@ watermark.selectText.6=heightSpacer (Space between each watermark vertically):
watermark.selectText.7=Opacity (0% - 100%): watermark.selectText.7=Opacity (0% - 100%):
watermark.selectText.8=Watermark Type: watermark.selectText.8=Watermark Type:
watermark.selectText.9=Watermark Image: watermark.selectText.9=Watermark Image:
watermark.selectText.10=Convert PDF to PDF-Image
watermark.submit=Add Watermark watermark.submit=Add Watermark
watermark.type.1=Text watermark.type.1=Text
watermark.type.2=Image watermark.type.2=Image
@@ -1125,3 +1142,9 @@ error.copyStack=Copy Stack Trace
error.githubSubmit=GitHub - Submit a ticket error.githubSubmit=GitHub - Submit a ticket
error.discordSubmit=Discord - Submit Support post error.discordSubmit=Discord - Submit Support post
#remove-image
removeImage.title=Remove image
removeImage.header=Remove image
removeImage.removeImage=Remove image
removeImage.submit=Remove image

View File

@@ -55,16 +55,18 @@ userNotFoundMessage=Usuario no encontrado.
incorrectPasswordMessage=La contraseña actual no es correcta. incorrectPasswordMessage=La contraseña actual no es correcta.
usernameExistsMessage=El nuevo nombre de usuario está en uso. usernameExistsMessage=El nuevo nombre de usuario está en uso.
invalidUsernameMessage=Nombre de usuario no válido, el nombre de usuario solo puede contener letras, números y los siguientes caracteres especiales @._+- o debe ser una dirección de correo electrónico válida. invalidUsernameMessage=Nombre de usuario no válido, el nombre de usuario solo puede contener letras, números y los siguientes caracteres especiales @._+- o debe ser una dirección de correo electrónico válida.
invalidPasswordMessage=The password must not be empty and must not have spaces at the beginning or end.
confirmPasswordErrorMessage=New Password and Confirm New Password must match. confirmPasswordErrorMessage=New Password and Confirm New Password must match.
deleteCurrentUserMessage=No puede eliminar el usuario que tiene la sesión actualmente en uso. deleteCurrentUserMessage=No puede eliminar el usuario que tiene la sesión actualmente en uso.
deleteUsernameExistsMessage=El usuario no existe y no puede eliminarse. deleteUsernameExistsMessage=El usuario no existe y no puede eliminarse.
downgradeCurrentUserMessage=No se puede degradar el rol del usuario actual downgradeCurrentUserMessage=No se puede degradar el rol del usuario actual
disabledCurrentUserMessage=The current user cannot be disabled
downgradeCurrentUserLongMessage=No se puede degradar el rol del usuario actual. Por lo tanto, el usuario actual no se mostrará. downgradeCurrentUserLongMessage=No se puede degradar el rol del usuario actual. Por lo tanto, el usuario actual no se mostrará.
userAlreadyExistsOAuthMessage=The user already exists as an OAuth2 user. userAlreadyExistsOAuthMessage=La usuario ya existe como usuario de OAuth2.
userAlreadyExistsWebMessage=The user already exists as an web user. userAlreadyExistsWebMessage=El usuario ya existe como usuario web.
error=Error error=Error
oops=Ups! oops=Ups!
help=Help help=Ayuda
goHomepage=Ir a la página principal goHomepage=Ir a la página principal
joinDiscord=Únase a nuestro servidor Discord joinDiscord=Únase a nuestro servidor Discord
seeDockerHub=Ver Docker Hub seeDockerHub=Ver Docker Hub
@@ -86,7 +88,7 @@ pipeline.defaultOption=Personalizar
pipeline.submitButton=Enviar pipeline.submitButton=Enviar
pipeline.help=Ayuda de Canalización pipeline.help=Ayuda de Canalización
pipeline.scanHelp=Ayuda de escaneado de carpetas pipeline.scanHelp=Ayuda de escaneado de carpetas
pipeline.deletePrompt=Are you sure you want to delete pipeline pipeline.deletePrompt=¿Estás segura de que quieres eliminar la canalización?
###################### ######################
# Pipeline Options # # Pipeline Options #
@@ -107,18 +109,18 @@ pipelineOptions.validateButton=Validar
############# #############
# NAVBAR # # NAVBAR #
############# #############
navbar.favorite=Favorites navbar.favorite=Favoritos
navbar.darkmode=Modo oscuro navbar.darkmode=Modo oscuro
navbar.language=Languages navbar.language=Idiomas
navbar.settings=Configuración navbar.settings=Configuración
navbar.allTools=Tools navbar.allTools=Herramientas
navbar.multiTool=Multi Tools navbar.multiTool=Multi herramientas
navbar.sections.organize=Organize navbar.sections.organize=Organize
navbar.sections.convertTo=Convert to PDF navbar.sections.convertTo=Convertir a PDF
navbar.sections.convertFrom=Convert from PDF navbar.sections.convertFrom=Convertir desde PDF
navbar.sections.security=Sign & Security navbar.sections.security=Señalización y seguridad
navbar.sections.advance=Advanced navbar.sections.advance=Avanzado
navbar.sections.edit=View & Edit navbar.sections.edit=Ver y Editar
############# #############
# SETTINGS # # SETTINGS #
@@ -175,8 +177,9 @@ adminUserSettings.header=Configuración de control de usuario administrador
adminUserSettings.admin=Administrador adminUserSettings.admin=Administrador
adminUserSettings.user=Usuario adminUserSettings.user=Usuario
adminUserSettings.addUser=Añadir Nuevo Usuario adminUserSettings.addUser=Añadir Nuevo Usuario
adminUserSettings.deleteUser=Delete User adminUserSettings.deleteUser=Eliminar Usuario
adminUserSettings.confirmDeleteUser=Should the user be deleted? adminUserSettings.confirmDeleteUser=¿Se debe eliminar al usuario?
adminUserSettings.confirmChangeUserStatus=Should the user be disabled/enabled?
adminUserSettings.usernameInfo=El nombre de usuario solo puede contener letras, números y los siguientes caracteres especiales @._+- o debe ser una dirección de correo electrónico válida. adminUserSettings.usernameInfo=El nombre de usuario solo puede contener letras, números y los siguientes caracteres especiales @._+- o debe ser una dirección de correo electrónico válida.
adminUserSettings.roles=Roles adminUserSettings.roles=Roles
adminUserSettings.role=Rol adminUserSettings.role=Rol
@@ -189,24 +192,31 @@ adminUserSettings.internalApiUser=Usuario interno de API
adminUserSettings.forceChange=Forzar usuario a cambiar usuario/contraseña en el acceso adminUserSettings.forceChange=Forzar usuario a cambiar usuario/contraseña en el acceso
adminUserSettings.submit=Guardar Usuario adminUserSettings.submit=Guardar Usuario
adminUserSettings.changeUserRole=Cambiar rol de usuario adminUserSettings.changeUserRole=Cambiar rol de usuario
adminUserSettings.authenticated=Authenticated adminUserSettings.authenticated=Autenticado
adminUserSettings.editOwnProfil=Edit own profile
adminUserSettings.enabledUser=enabled user
adminUserSettings.disabledUser=disabled user
adminUserSettings.activeUsers=Active Users:
adminUserSettings.disabledUsers=Disabled Users:
adminUserSettings.totalUsers=Total Users:
adminUserSettings.lastRequest=Last Request
database.title=Database Import/Export database.title=Base de Datos Importar/Exportar
database.header=Database Import/Export database.header=Base de Datos Importar/Exportar
database.fileName=File Name database.fileName=Nombre de Archivo
database.creationDate=Creation Date database.creationDate=Fecha de creación
database.fileSize=File Size database.fileSize=Tamaño de archivo
database.deleteBackupFile=Delete Backup File database.deleteBackupFile=Eliminar archivo de copia de seguridad
database.importBackupFile=Import Backup File database.importBackupFile=Importar archivo de copia de seguridad
database.downloadBackupFile=Download Backup File database.downloadBackupFile=Descargar archivo de copia de seguridad
database.info_1=When importing data, it is crucial to ensure the correct structure. If you are unsure of what you are doing, seek advice and support from a professional. An error in the structure can cause application malfunctions, up to and including the complete inability to run the application. database.info_1=Al importar datos, es fundamental garantizar la estructura correcta. Si no está seguro de lo que está haciendo, busque consejo y apoyo de un profesional. Un error en la estructura puede causar un mal funcionamiento de la aplicación, incluyendo la imposibilidad total de ejecutar la aplicación.
database.info_2=The file name does not matter when uploading. It will be renamed afterward to follow the format backup_user_yyyyMMddHHmm.sql, ensuring a consistent naming convention. database.info_2=El nombre del archivo no importa al cargarlo. Posteriormente se le cambiará el nombre para que siga el formato backup_user_yyyyMMddHHmm.sql, lo que garantiza una convención de nomenclatura coherente.
database.submit=Import Backup database.submit=Importar Backup
database.importIntoDatabaseSuccessed=Import into database successed database.importIntoDatabaseSuccessed=Importación a la base de datos ha sido exitosa
database.fileNotFound=File not Found database.fileNotFound=Archivo no encontrado
database.fileNullOrEmpty=File must not be null or empty database.fileNullOrEmpty=El archivo no debe ser nulo o vacío.
database.failedImportFile=Failed Import File database.failedImportFile=Archivo de importación fallido
############# #############
# HOME-PAGE # # HOME-PAGE #
@@ -353,8 +363,8 @@ home.certSign.title=Firmar con certificado
home.certSign.desc=Firmar un PDF con un Certificado/Clave (PEM/P12) home.certSign.desc=Firmar un PDF con un Certificado/Clave (PEM/P12)
certSign.tags=autentificar,PEM,P12,oficial,encriptar certSign.tags=autentificar,PEM,P12,oficial,encriptar
home.removeCertSign.title=Remove Certificate Sign home.removeCertSign.title=Quitar signo de certificado
home.removeCertSign.desc=Remove certificate signature from PDF home.removeCertSign.desc=Eliminar firma de certificado de PDF
removeCertSign.tags=authenticate,PEM,P12,official,decrypt removeCertSign.tags=authenticate,PEM,P12,official,decrypt
home.pageLayout.title=Diseño de varias páginas home.pageLayout.title=Diseño de varias páginas
@@ -461,6 +471,10 @@ home.BookToPDF.title=Libro a PDF
home.BookToPDF.desc=Convierte formatos de Libro/Cómic a PDF usando Calibre home.BookToPDF.desc=Convierte formatos de Libro/Cómic a PDF usando Calibre
BookToPDF.tags=Libro,Cómic,Calibre,Convertir,manga,Amazon,Kindle BookToPDF.tags=Libro,Cómic,Calibre,Convertir,manga,Amazon,Kindle
home.removeImagePdf.title=Remove image
home.removeImagePdf.desc=Remove image from PDF to reduce file size
removeImagePdf.tags=Remove Image,Page operations,Back end,server side
########################### ###########################
# # # #
@@ -476,13 +490,15 @@ login.invalid=Nombre de usuario o contraseña erróneos.
login.locked=Su cuenta se ha bloqueado. login.locked=Su cuenta se ha bloqueado.
login.signinTitle=Por favor, inicie sesión login.signinTitle=Por favor, inicie sesión
login.ssoSignIn=Iniciar sesión a través del inicio de sesión único login.ssoSignIn=Iniciar sesión a través del inicio de sesión único
login.oauth2AutoCreateDisabled=Usuario DE creación automática de OAUTH2 DESACTIVADO login.oauth2AutoCreateDisabled=Usuario de creación automática de OAUTH2 DESACTIVADO
login.oauth2RequestNotFound=Authorization request not found login.oauth2AdminBlockedUser=Registration or logging in of non-registered users is currently blocked. Please contact the administrator.
login.oauth2InvalidUserInfoResponse=Invalid User Info Response login.oauth2RequestNotFound=Solicitud de autorización no encontrada
login.oauth2InvalidUserInfoResponse=Respuesta de información de usuario no válida
login.oauth2invalidRequest=Invalid Request login.oauth2invalidRequest=Invalid Request
login.oauth2AccessDenied=Access Denied login.oauth2AccessDenied=Access Denied
login.oauth2InvalidTokenResponse=Invalid Token Response login.oauth2InvalidTokenResponse=Respuesta de token no válida
login.oauth2InvalidIdToken=Invalid Id Token login.oauth2InvalidIdToken=Token de identificación no válido
login.userIsDisabled=User is deactivated, login is currently blocked with this username. Please contact the administrator.
#auto-redact #auto-redact
@@ -681,10 +697,10 @@ certSign.submit=Firmar PDF
#removeCertSign #removeCertSign
removeCertSign.title=Remove Certificate Signature removeCertSign.title=Eliminar firma del certificado
removeCertSign.header=Remove the digital certificate from the PDF removeCertSign.header=Quitar el certificado digital del PDF
removeCertSign.selectPDF=Select a PDF file: removeCertSign.selectPDF=Seleccione un archivo PDF:
removeCertSign.submit=Remove Signature removeCertSign.submit=Eliminar firma
#removeBlanks #removeBlanks
@@ -706,8 +722,8 @@ removeAnnotations.submit=Eliminar
#compare #compare
compare.title=Comparar compare.title=Comparar
compare.header=Comparar archivos PDF compare.header=Comparar archivos PDF
compare.highlightColor.1=Highlight Color 1: compare.highlightColor.1=Color resaltado 1:
compare.highlightColor.2=Highlight Color 2: compare.highlightColor.2=Color resaltado 2:
compare.document.1=Documento 1 compare.document.1=Documento 1
compare.document.2=Documento 2 compare.document.2=Documento 2
compare.submit=Comparar compare.submit=Comparar
@@ -744,7 +760,7 @@ repair.submit=Reparar
#flatten #flatten
flatten.title=Aplanar flatten.title=Aplanar
flatten.header=Acoplar archivos PDF flatten.header=Acoplar archivos PDF
flatten.flattenOnlyForms=Flatten only forms flatten.flattenOnlyForms=Aplanar sólo formularios
flatten.submit=Aplanar flatten.submit=Aplanar
@@ -792,7 +808,7 @@ extractImages.submit=Extraer
fileToPDF.title=Archivo a PDF fileToPDF.title=Archivo a PDF
fileToPDF.header=Convertir cualquier archivo a PDF fileToPDF.header=Convertir cualquier archivo a PDF
fileToPDF.credit=Este servicio usa LibreOffice y Unoconv para la conversión de archivos fileToPDF.credit=Este servicio usa LibreOffice y Unoconv para la conversión de archivos
fileToPDF.supportedFileTypesInfo=Supported File types fileToPDF.supportedFileTypesInfo=Tipos de archivos admitidos
fileToPDF.supportedFileTypes=Los tipos de archivo soportados deben incluir los indicados a continuación; sin embargo, para una completa y acutualizada lista de formatos soportados, por favor consulte la documentación de LibreOffice fileToPDF.supportedFileTypes=Los tipos de archivo soportados deben incluir los indicados a continuación; sin embargo, para una completa y acutualizada lista de formatos soportados, por favor consulte la documentación de LibreOffice
fileToPDF.submit=Convertir a PDF fileToPDF.submit=Convertir a PDF
@@ -945,6 +961,7 @@ watermark.selectText.6=Alto (Espacio entre cada marca de agua verticalmente):
watermark.selectText.7=Opacidad (0% - 100%): watermark.selectText.7=Opacidad (0% - 100%):
watermark.selectText.8=Tipo de marca de agua: watermark.selectText.8=Tipo de marca de agua:
watermark.selectText.9=Imagen de marca de agua: watermark.selectText.9=Imagen de marca de agua:
watermark.selectText.10=Convert PDF to PDF-Image
watermark.submit=Añadir marca de agua watermark.submit=Añadir marca de agua
watermark.type.1=Texto watermark.type.1=Texto
watermark.type.2=Imagen watermark.type.2=Imagen
@@ -1000,8 +1017,8 @@ pdfToPDFA.header=PDF a PDF/A
pdfToPDFA.credit=Este servicio usa OCRmyPDF para la conversión a PDF/A pdfToPDFA.credit=Este servicio usa OCRmyPDF para la conversión a PDF/A
pdfToPDFA.submit=Convertir pdfToPDFA.submit=Convertir
pdfToPDFA.tip=Actualmente no funciona para múltiples entrada a la vez pdfToPDFA.tip=Actualmente no funciona para múltiples entrada a la vez
pdfToPDFA.outputFormat=Output format pdfToPDFA.outputFormat=Formato de salida
pdfToPDFA.pdfWithDigitalSignature=The PDF contains a digital signature. This will be removed in the next step. pdfToPDFA.pdfWithDigitalSignature=El PDF contiene una firma digital. Esto se eliminará en el siguiente paso.
#PDFToWord #PDFToWord
@@ -1103,13 +1120,13 @@ licenses.version=Versión
licenses.license=Licencia licenses.license=Licencia
#survey #survey
survey.nav=Survey survey.nav=Encuesta
survey.title=Stirling-PDF Survey survey.title=Encuesta Stirling-PDF
survey.description=Stirling-PDF has no tracking so we want to hear from our users to improve Stirling-PDF! survey.description=Stirling-PDF no tiene seguimiento, por lo que queremos escuchar a nuestros usuarios para mejorar Stirling-PDF.
survey.please=Please consider taking our survey! survey.please=¡Considere realizar nuestra encuesta!
survey.disabled=(Survey popup will be disabled in following updates but available at foot of page) survey.disabled=(La ventana emergente de la encuesta se desactivará en las siguientes actualizaciones, pero estará disponible al pie de la página.)
survey.button=Take Survey survey.button=Realizar encuesta
survey.dontShowAgain=Don't show again survey.dontShowAgain=No volver a mostrar
#error #error
@@ -1125,3 +1142,9 @@ error.copyStack=Mostrar seguimiento de pila
error.githubSubmit=GitHub - Enviar un ticket error.githubSubmit=GitHub - Enviar un ticket
error.discordSubmit=Discord - Enviar mensaje de soporte error.discordSubmit=Discord - Enviar mensaje de soporte
#remove-image
removeImage.title=Remove image
removeImage.header=Remove image
removeImage.removeImage=Remove image
removeImage.submit=Remove image

View File

@@ -55,10 +55,12 @@ userNotFoundMessage=User not found.
incorrectPasswordMessage=Current password is incorrect. incorrectPasswordMessage=Current password is incorrect.
usernameExistsMessage=New Username already exists. usernameExistsMessage=New Username already exists.
invalidUsernameMessage=Invalid username, username can only contain letters, numbers and the following special characters @._+- or must be a valid email address. invalidUsernameMessage=Invalid username, username can only contain letters, numbers and the following special characters @._+- or must be a valid email address.
invalidPasswordMessage=The password must not be empty and must not have spaces at the beginning or end.
confirmPasswordErrorMessage=New Password and Confirm New Password must match. confirmPasswordErrorMessage=New Password and Confirm New Password must match.
deleteCurrentUserMessage=Cannot delete currently logged in user. deleteCurrentUserMessage=Cannot delete currently logged in user.
deleteUsernameExistsMessage=The username does not exist and cannot be deleted. deleteUsernameExistsMessage=The username does not exist and cannot be deleted.
downgradeCurrentUserMessage=Ezin da uneko erabiltzailearen rola jaitsi downgradeCurrentUserMessage=Ezin da uneko erabiltzailearen rola jaitsi
disabledCurrentUserMessage=The current user cannot be disabled
downgradeCurrentUserLongMessage=Ezin da uneko erabiltzailearen rola jaitsi. Beraz, oraingo erabiltzailea ez da erakutsiko. downgradeCurrentUserLongMessage=Ezin da uneko erabiltzailearen rola jaitsi. Beraz, oraingo erabiltzailea ez da erakutsiko.
userAlreadyExistsOAuthMessage=The user already exists as an OAuth2 user. userAlreadyExistsOAuthMessage=The user already exists as an OAuth2 user.
userAlreadyExistsWebMessage=The user already exists as an web user. userAlreadyExistsWebMessage=The user already exists as an web user.
@@ -177,6 +179,7 @@ adminUserSettings.user=Erabiltzaile
adminUserSettings.addUser=Erabiltzaile berria adminUserSettings.addUser=Erabiltzaile berria
adminUserSettings.deleteUser=Delete User adminUserSettings.deleteUser=Delete User
adminUserSettings.confirmDeleteUser=Should the user be deleted? adminUserSettings.confirmDeleteUser=Should the user be deleted?
adminUserSettings.confirmChangeUserStatus=Should the user be disabled/enabled?
adminUserSettings.usernameInfo=Username can only contain letters, numbers and the following special characters @._+- or must be a valid email address. adminUserSettings.usernameInfo=Username can only contain letters, numbers and the following special characters @._+- or must be a valid email address.
adminUserSettings.roles=Rolak adminUserSettings.roles=Rolak
adminUserSettings.role=Rol adminUserSettings.role=Rol
@@ -190,6 +193,13 @@ adminUserSettings.forceChange=Force user to change password on login
adminUserSettings.submit=Gorde Erabiltzailea adminUserSettings.submit=Gorde Erabiltzailea
adminUserSettings.changeUserRole=Erabiltzailearen rola aldatu adminUserSettings.changeUserRole=Erabiltzailearen rola aldatu
adminUserSettings.authenticated=Authenticated adminUserSettings.authenticated=Authenticated
adminUserSettings.editOwnProfil=Edit own profile
adminUserSettings.enabledUser=enabled user
adminUserSettings.disabledUser=disabled user
adminUserSettings.activeUsers=Active Users:
adminUserSettings.disabledUsers=Disabled Users:
adminUserSettings.totalUsers=Total Users:
adminUserSettings.lastRequest=Last Request
database.title=Database Import/Export database.title=Database Import/Export
@@ -461,6 +471,10 @@ home.BookToPDF.title=Book to PDF
home.BookToPDF.desc=Converts Books/Comics formats to PDF using calibre home.BookToPDF.desc=Converts Books/Comics formats to PDF using calibre
BookToPDF.tags=Book,Comic,Calibre,Convert,manga,amazon,kindle BookToPDF.tags=Book,Comic,Calibre,Convert,manga,amazon,kindle
home.removeImagePdf.title=Remove image
home.removeImagePdf.desc=Remove image from PDF to reduce file size
removeImagePdf.tags=Remove Image,Page operations,Back end,server side
########################### ###########################
# # # #
@@ -477,12 +491,14 @@ login.locked=Zure kontua blokeatu egin da.
login.signinTitle=Mesedez, hasi saioa login.signinTitle=Mesedez, hasi saioa
login.ssoSignIn=Hasi saioa Saioa hasteko modu bakarraren bidez login.ssoSignIn=Hasi saioa Saioa hasteko modu bakarraren bidez
login.oauth2AutoCreateDisabled=OAUTH2 Sortu automatikoki erabiltzailea desgaituta dago login.oauth2AutoCreateDisabled=OAUTH2 Sortu automatikoki erabiltzailea desgaituta dago
login.oauth2AdminBlockedUser=Registration or logging in of non-registered users is currently blocked. Please contact the administrator.
login.oauth2RequestNotFound=Authorization request not found login.oauth2RequestNotFound=Authorization request not found
login.oauth2InvalidUserInfoResponse=Invalid User Info Response login.oauth2InvalidUserInfoResponse=Invalid User Info Response
login.oauth2invalidRequest=Invalid Request login.oauth2invalidRequest=Invalid Request
login.oauth2AccessDenied=Access Denied login.oauth2AccessDenied=Access Denied
login.oauth2InvalidTokenResponse=Invalid Token Response login.oauth2InvalidTokenResponse=Invalid Token Response
login.oauth2InvalidIdToken=Invalid Id Token login.oauth2InvalidIdToken=Invalid Id Token
login.userIsDisabled=User is deactivated, login is currently blocked with this username. Please contact the administrator.
#auto-redact #auto-redact
@@ -945,6 +961,7 @@ watermark.selectText.6=Altuera (ur-marka bakoitzaren arteko espazioa bertikalean
watermark.selectText.7=Opakutasuna (0% - 100%): watermark.selectText.7=Opakutasuna (0% - 100%):
watermark.selectText.8=Watermark Type: watermark.selectText.8=Watermark Type:
watermark.selectText.9=Watermark Image: watermark.selectText.9=Watermark Image:
watermark.selectText.10=Convert PDF to PDF-Image
watermark.submit=Gehitu ur-marka watermark.submit=Gehitu ur-marka
watermark.type.1=Text watermark.type.1=Text
watermark.type.2=Image watermark.type.2=Image
@@ -1125,3 +1142,9 @@ error.copyStack=Copy Stack Trace
error.githubSubmit=GitHub - Submit a ticket error.githubSubmit=GitHub - Submit a ticket
error.discordSubmit=Discord - Submit Support post error.discordSubmit=Discord - Submit Support post
#remove-image
removeImage.title=Remove image
removeImage.header=Remove image
removeImage.removeImage=Remove image
removeImage.submit=Remove image

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