Compare commits

...

194 Commits

Author SHA1 Message Date
Anthony Stirling
8dca4a588d Merge pull request #2505 from reecebrowne/toolbar-tweaks2
Toolbar tweaks2
2024-12-19 13:22:41 +00:00
Reece Browne
63386baa0d Conditional logic for stacking columns 2024-12-19 12:58:57 +00:00
Reece Browne
2a93910da3 Remove padding 2024-12-19 12:04:08 +00:00
Reece Browne
fca6dc1fd8 Stack convert section, don't remove. Move remove into own js 2024-12-19 11:59:58 +00:00
Anthony Stirling
2d82c5fa77 Merge pull request #2503 from omar-ahmed42/fix-page-resize-in-redact-img-conversion
Fix: Draw image with the original PDF page dimensions
2024-12-19 11:49:18 +00:00
Omar Ahmed Hassan
167c792bf0 Draw image with the original PDF page dimensions 2024-12-19 13:34:13 +02:00
Anthony Stirling
95a9e10dc8 Update releaseArtifacts.yml 2024-12-19 10:52:41 +00:00
Anthony Stirling
4d6368048c Update build.gradle 2024-12-18 21:28:23 +00:00
Anthony Stirling
4e715a82e0 Merge pull request #2501 from Ludy87/missing_pdflib
[Bugfix] A variety of tools miss PDFLib
2024-12-18 21:12:08 +00:00
Ludy87
164381e940 some tools lack PDFLib 2024-12-18 21:03:08 +01:00
Anthony Stirling
0436f45de5 Merge pull request #2500 from Stirling-Tools/configCheck
Config mount check
2024-12-18 18:14:46 +00:00
Anthony Stirling
6ce761aff2 Merge remote-tracking branch 'origin/main' into configCheck 2024-12-18 18:05:16 +00:00
Anthony Stirling
8a5d9f9a95 mounted_config_dir 2024-12-18 18:04:10 +00:00
Anthony Stirling
c0ef624a1d Merge pull request #2499 from albanobattistella/patch-62
Update messages_it_IT.properties
2024-12-18 14:42:48 +00:00
albanobattistella
b9ae90274f Update messages_it_IT.properties 2024-12-18 15:34:39 +01:00
Anthony Stirling
764b8f4d22 Merge pull request #2498 from Ludy87/code_refactoring
Exclude Sensitive and Error-Prone Fields from toString() in SAML2 Con…
2024-12-18 12:58:13 +00:00
Ludy87
a531f53893 Exclude Sensitive and Error-Prone Fields from toString() in SAML2 Configuration 2024-12-18 13:40:24 +01:00
Anthony Stirling
74d6d96f4e Merge pull request #2495 from Stirling-Tools/sync_readme
📝 Update README: Translation Progress Table
2024-12-18 12:18:17 +00:00
Anthony Stirling
1862ab1671 Merge pull request #2497 from reecebrowne/File-input-append
Remove file input append
2024-12-18 12:18:02 +00:00
Reece Browne
bf95ca43dc Remove append entirely 2024-12-18 11:47:23 +00:00
Reece Browne
678ef85da1 Don't remove files from file input, just append 2024-12-18 11:09:52 +00:00
Anthony Stirling
9a6afdd921 Merge pull request #2493 from reecebrowne/bug/2490/2488/image-to-pdf
Img to pdf bug fixes
2024-12-18 10:42:34 +00:00
reecebrowne
a72615cc86 Merge branch 'main' into bug/2490/2488/image-to-pdf 2024-12-18 10:40:54 +00:00
Anthony Stirling
b0daac2566 Merge pull request #2492 from omar-ahmed42/fix-no-formdata-sent-in-multi
Fix: form data not being sent in multi
2024-12-18 10:37:31 +00:00
github-actions[bot]
2335ecf017 📝 Sync README
> Made via sync_files.yml
2024-12-18 10:34:08 +00:00
Anthony Stirling
a79318bf0c Merge pull request #2491 from Ludy87/database_create_m
Adds manual backup creation
2024-12-18 10:33:50 +00:00
Reece Browne
9eed761346 Correct default fit 2024-12-18 00:36:04 +00:00
Reece Browne
12d86049f6 Add default to convert image to pdf api 2024-12-18 00:30:06 +00:00
Reece Browne
e9f80d03ea Fix missing info in form data 2024-12-18 00:16:01 +00:00
Omar Ahmed Hassan
42c238d87d Pass form to submitMultiPdfForm
- Pass form to submitMultiPdfForm to use in FormData creation.
2024-12-18 01:28:04 +02:00
Ludy87
051cd2d0d5 Adds manual backup creation 2024-12-17 23:23:16 +01:00
Anthony Stirling
eb1301464d Update build.gradle 2024-12-17 13:59:30 +00:00
Anthony Stirling
720705e422 Merge pull request #2466 from reecebrowne/bug/fix-multitool-drag
Fix drag and drop bugs and clean up UI
2024-12-17 13:53:17 +00:00
Anthony Stirling
db3a8a8483 Merge branch 'main' into bug/fix-multitool-drag 2024-12-17 13:51:51 +00:00
Reece Browne
2bdda9acba Fix drop on self bug
Fix highlight fadeout bug
UI tweaks
2024-12-17 13:47:44 +00:00
Anthony Stirling
8053035158 Merge pull request #2485 from omar-ahmed42/fix-2478-draggableElement-null
Fix draggableElement is null by removing draggable.js
2024-12-17 11:16:22 +00:00
Anthony Stirling
bd20d3ad93 Merge branch 'main' into fix-2478-draggableElement-null 2024-12-17 11:14:59 +00:00
Anthony Stirling
c6700b34b0 Merge pull request #2484 from Stirling-Tools/sync_readme
📝 Update README: Translation Progress Table
2024-12-17 10:51:11 +00:00
github-actions[bot]
25b66a0775 📝 Sync README
> Made via sync_files.yml
2024-12-17 10:48:55 +00:00
Anthony Stirling
ac9f4a4c7e Merge pull request #2483 from Ludy87/german__17_12
Update messages_de_DE.properties
2024-12-17 10:48:38 +00:00
Ludy
3a0d89bd7c Merge branch 'main' into german__17_12 2024-12-17 11:47:44 +01:00
Omar Ahmed Hassan
5d4769df56 Remove unused draggable.js file
- Remove draggable.js as the draggability feature already works without and it wasn't used anywhere except in sign.html (which throw an exception already as draggableElement was null, and yet the functionality was working as expected, thus we don't need it)
2024-12-17 12:47:13 +02:00
Ludy87
80b9d2691a Update messages_de_DE.properties 2024-12-17 11:46:50 +01:00
Anthony Stirling
99c598096e Merge pull request #2482 from Stirling-Tools/update-3rd-party-licenses
Update 3rd Party Licenses
2024-12-17 10:46:06 +00:00
Anthony Stirling
b3dfdba9cb Merge pull request #2476 from omar-ahmed42/fix-img-to-pdf-2456
Fix img to pdf merge conversion type
2024-12-17 10:36:38 +00:00
Anthony Stirling
c2e96c5f1d Merge branch 'main' into fix-img-to-pdf-2456 2024-12-17 10:35:22 +00:00
GitHub Action
cffdbc5e7b Update 3rd Party Licenses
Signed-off-by: GitHub Action <action@github.com>
2024-12-17 10:28:26 +00:00
Anthony Stirling
e8d7b4aded Merge pull request #2472 from Stirling-Tools/dependabot/gradle/org.springframework-spring-webmvc-6.2.1
Bump org.springframework:spring-webmvc from 6.2.0 to 6.2.1
2024-12-17 10:27:49 +00:00
dependabot[bot]
d672ad22bc Bump org.springframework:spring-webmvc from 6.2.0 to 6.2.1
Bumps [org.springframework:spring-webmvc](https://github.com/spring-projects/spring-framework) from 6.2.0 to 6.2.1.
- [Release notes](https://github.com/spring-projects/spring-framework/releases)
- [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.0...v6.2.1)

---
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>
2024-12-17 10:26:24 +00:00
Anthony Stirling
2c0a9ae082 Merge pull request #2469 from Stirling-Tools/dependabot/gradle/io.micrometer-micrometer-core-1.14.2
Bump io.micrometer:micrometer-core from 1.14.1 to 1.14.2
2024-12-17 10:25:35 +00:00
dependabot[bot]
bbedf22678 Bump io.micrometer:micrometer-core from 1.14.1 to 1.14.2
Bumps [io.micrometer:micrometer-core](https://github.com/micrometer-metrics/micrometer) from 1.14.1 to 1.14.2.
- [Release notes](https://github.com/micrometer-metrics/micrometer/releases)
- [Commits](https://github.com/micrometer-metrics/micrometer/compare/v1.14.1...v1.14.2)

---
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>
2024-12-17 10:24:19 +00:00
Anthony Stirling
e9431ba6b5 Merge pull request #2474 from Stirling-Tools/dependabot/gradle/org.springframework.security-spring-security-saml2-service-provider-6.4.2
Bump org.springframework.security:spring-security-saml2-service-provider from 6.4.1 to 6.4.2
2024-12-17 10:23:37 +00:00
dependabot[bot]
d74c521bc0 Bump org.springframework.security:spring-security-saml2-service-provider
Bumps [org.springframework.security:spring-security-saml2-service-provider](https://github.com/spring-projects/spring-security) from 6.4.1 to 6.4.2.
- [Release notes](https://github.com/spring-projects/spring-security/releases)
- [Changelog](https://github.com/spring-projects/spring-security/blob/main/RELEASE.adoc)
- [Commits](https://github.com/spring-projects/spring-security/compare/6.4.1...6.4.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-17 10:22:25 +00:00
Anthony Stirling
fa57ee4ef9 Merge pull request #2471 from Stirling-Tools/dependabot/gradle/io.github.pixee-java-security-toolkit-1.2.1
Bump io.github.pixee:java-security-toolkit from 1.2.0 to 1.2.1
2024-12-17 10:21:39 +00:00
dependabot[bot]
be3cbcc7cd Bump io.github.pixee:java-security-toolkit from 1.2.0 to 1.2.1
Bumps [io.github.pixee:java-security-toolkit](https://github.com/pixee/java-security-toolkit) from 1.2.0 to 1.2.1.
- [Release notes](https://github.com/pixee/java-security-toolkit/releases)
- [Commits](https://github.com/pixee/java-security-toolkit/compare/v1.2.0...1.2.1)

---
updated-dependencies:
- dependency-name: io.github.pixee:java-security-toolkit
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-17 10:20:25 +00:00
Anthony Stirling
317d985166 Merge pull request #2481 from Ludy87/bump_googleJavaFormat_1_25_2
Bump googleJavaFormat from 1.22.0 to 1.25.2
2024-12-17 10:19:37 +00:00
Anthony Stirling
64f8348215 Merge branch 'main' into bump_googleJavaFormat_1_25_2 2024-12-17 10:18:20 +00:00
Anthony Stirling
7f045dbcc7 Merge pull request #2480 from Ludy87/logger
Remove Direct Logger and Use Lombok `@Slf4j`
2024-12-17 10:17:51 +00:00
Ludy87
343e38a5fd Bump googleJavaFormat from 1.22.0 to 1.25.2 2024-12-17 11:01:17 +01:00
Ludy87
af100d4190 Remove Direct Logger and Use Lombok @Slf4j 2024-12-17 10:26:18 +01:00
Omar Ahmed Hassan
4998ad064a Fix form data files being replaced by the last decrypted file 2024-12-17 01:01:15 +02:00
Omar Ahmed Hassan
64ee26facf Use file-input-change event to disable/enable conversionType 2024-12-17 01:00:21 +02:00
Anthony Stirling
e273b9a3ad Merge pull request #2468 from omar-ahmed42/fix-langs-env-override
Fix: Rename LANGS variable in init.sh to avoid clashing with LANGS env
2024-12-16 22:41:13 +00:00
Omar Ahmed Hassan
42541a7174 Rename LANGS variable in init.sh to avoid overriding font LANGS
Rename LANGS variable in init.sh for TESSERACT_LANGS languages as it conflicts with the variable LANGS that is responsible for installing fonts
2024-12-16 23:55:03 +02:00
Anthony Stirling
ca3002f925 Merge branch 'main' into bug/fix-multitool-drag 2024-12-16 21:21:10 +00:00
Anthony Stirling
6c172af3c1 Merge pull request #2467 from Stirling-Tools/Frooodle-patch-2
update python version used in pre commits
2024-12-16 21:21:01 +00:00
Anthony Stirling
60ced19f64 Update build.yml 2024-12-16 20:59:47 +00:00
Reece Browne
88079ddf7f Reduce opacity of dragged page on multidrag not just single 2024-12-16 19:37:15 +00:00
Reece Browne
9b6dcdcd06 Fix drag and drop bugs and clean up UI 2024-12-16 19:07:15 +00:00
Anthony Stirling
63eb94c0a6 Merge pull request #2463 from reecebrowne/bug/fix_merge
Add missing pdflib
2024-12-16 14:11:36 +00:00
Reece Browne
0a6b6453b2 Update build number 2024-12-16 14:03:38 +00:00
Reece Browne
2931e348a9 Add missing pdflib 2024-12-16 13:55:44 +00:00
Anthony Stirling
c71ca21532 Update multiOSReleases.yml 2024-12-14 11:08:35 +00:00
Anthony Stirling
0e4c3d5dbc Update releaseArtifacts.yml 2024-12-14 11:01:49 +00:00
Anthony Stirling
92cabf125e Merge pull request #2453 from Stirling-Tools/csrf2
Csrf fixes
2024-12-14 10:59:50 +00:00
Anthony Stirling
818bed3154 Update build.gradle 2024-12-14 10:43:35 +00:00
a
faf3454a02 Merge remote-tracking branch 'origin/main' into csrf2 2024-12-14 10:42:26 +00:00
Anthony Stirling
1f1c414138 csrf fixes 2024-12-14 10:42:07 +00:00
Anthony Stirling
f1c5384a37 Merge pull request #2445 from Stirling-Tools/sync_readme
📝 Update README: Translation Progress Table
2024-12-13 20:33:21 +00:00
Anthony Stirling
5607f7079e Merge pull request #2441 from reecebrowne/bug/csrf-decryption-API
CSRF token for decryption
2024-12-13 20:33:03 +00:00
Anthony Stirling
3b8723975d Merge branch 'main' into bug/csrf-decryption-API 2024-12-13 20:31:49 +00:00
Anthony Stirling
41a3d28c90 Merge pull request #2439 from Stirling-Tools/update-3rd-party-licenses
Update 3rd Party Licenses
2024-12-13 20:31:23 +00:00
GitHub Action
f7afe73cb4 Update 3rd Party Licenses
Signed-off-by: GitHub Action <action@github.com>
2024-12-13 20:26:32 +00:00
github-actions[bot]
73e5246191 📝 Sync README
> Made via sync_files.yml
2024-12-13 20:25:58 +00:00
Anthony Stirling
1f39481efe Merge branch 'main' into bug/csrf-decryption-API 2024-12-13 20:25:52 +00:00
Anthony Stirling
5ac2260d78 Merge pull request #2451 from Stirling-Tools/testStuff Full local webUI client
Full local webUI client
2024-12-13 20:25:39 +00:00
Anthony Stirling
bae83a281c Update Dockerfile 2024-12-13 20:07:45 +00:00
Anthony Stirling
dd2aae60ad Update Dockerfile-fat 2024-12-13 20:07:30 +00:00
Anthony Stirling
30ee33002d Merge branch 'main' into testStuff 2024-12-13 19:31:53 +00:00
Anthony Stirling
24717dde19 finish 2024-12-13 18:20:54 +00:00
Anthony Stirling
509a305985 logs and cleanup 2024-12-13 16:58:34 +00:00
Anthony Stirling
13572a7f18 remove non windows for now 2024-12-13 16:04:45 +00:00
Anthony Stirling
ebd0ddc6ad test 2024-12-13 12:14:21 +00:00
Anthony Stirling
43c4ec1089 fixes! 2024-12-13 11:31:49 +00:00
Anthony Stirling
1ccdc1697b dif main class 2024-12-13 10:49:04 +00:00
Anthony Stirling
2297c5dc95 Merge pull request #2442 from lhui/main
add and refactor CN translate
2024-12-13 10:29:13 +00:00
Anthony Stirling
859c9942e6 Merge pull request #2444 from tkymmm/main
Update messages_ja_JP.properties
2024-12-13 10:28:57 +00:00
Anthony Stirling
c723696bd0 Merge pull request #2443 from albanobattistella/patch-61
Update messages_it_IT.properties
2024-12-13 10:28:46 +00:00
Anthony Stirling
fe198458ca fix main class 2024-12-13 10:28:03 +00:00
tkymmm
378aca4460 Update messages_ja_JP.properties 2024-12-13 13:23:26 +09:00
Anthony Stirling
9870e6ad7c fix 2024-12-13 00:59:42 +00:00
Anthony Stirling
40b5904726 test 2024-12-13 00:52:12 +00:00
Anthony Stirling
de3f59cf44 destination = "${projectDir}/build/jpackage" 2024-12-13 00:47:37 +00:00
Anthony Stirling
4ae7c83357 tests 2024-12-13 00:44:32 +00:00
Anthony Stirling
f127271709 test 2024-12-13 00:31:39 +00:00
Anthony Stirling
f899088c75 test 2024-12-13 00:29:03 +00:00
Anthony Stirling
d55e007f93 test dirs 2024-12-13 00:24:51 +00:00
Anthony Stirling
ccdcb05e65 x test 2024-12-13 00:13:47 +00:00
Anthony Stirling
e6c2ad8c9c test 2024-12-13 00:00:56 +00:00
Anthony Stirling
32aa623c8b test 2024-12-12 23:57:12 +00:00
Anthony Stirling
23888c5d2c test 2024-12-12 23:50:51 +00:00
Anthony Stirling
2f23eb69c6 naming 2024-12-12 23:40:49 +00:00
Anthony Stirling
446cedf26e unix name 2024-12-12 23:26:23 +00:00
Anthony Stirling
f950e25dad versioning 2024-12-12 23:22:19 +00:00
Anthony Stirling
65a86a6e97 remove mac 18+ 2024-12-12 23:16:27 +00:00
Anthony Stirling
7f4134f52d info 2024-12-12 23:08:28 +00:00
Anthony Stirling
55969697b8 build 2024-12-12 23:05:59 +00:00
Anthony Stirling
86662d9cf6 more testing 2024-12-12 23:03:42 +00:00
albanobattistella
2437460c73 Update messages_it_IT.properties 2024-12-12 19:00:26 +01:00
lihui
a7960d992c add and refactor CN translate 2024-12-13 00:42:18 +08:00
Reece Browne
c98cd8117f CSRF token for decryption 2024-12-12 13:07:50 +00:00
Anthony Stirling
50c5efac87 Merge pull request #2440 from MaratheHarshad/fix/collapsible-menus-on-reload
Fix collapsed menu issue on page reload
2024-12-12 11:48:54 +00:00
Anthony Stirling
0952245b23 Merge pull request #2425 from Stirling-Tools/dependabot/docker/alpine-3.21.0
Bump alpine from 3.20.3 to 3.21.0
2024-12-12 11:32:16 +00:00
dependabot[bot]
83ddfdf152 Bump alpine from 3.20.3 to 3.21.0
Bumps alpine from 3.20.3 to 3.21.0.

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-12 11:29:57 +00:00
Anthony Stirling
08777c78b1 Merge pull request #2423 from Stirling-Tools/dependabot/gradle/org.thymeleaf.extras-thymeleaf-extras-springsecurity5-3.1.3.RELEASE
Bump org.thymeleaf.extras:thymeleaf-extras-springsecurity5 from 3.1.2.RELEASE to 3.1.3.RELEASE
2024-12-12 11:29:22 +00:00
Anthony Stirling
c6980e9693 Merge pull request #2434 from Ludy87/security_fix_1
Security fix: Server-Side Request Forgery
2024-12-12 11:28:56 +00:00
Anthony Stirling
fc514ee65b Merge pull request #2436 from Stirling-Tools/snyk-fix-9e2683c5d747da5ff97f3f913479b288
[Snyk] Security upgrade alpine from 3.20.3 to 3.21.0
2024-12-12 11:28:37 +00:00
Harshad Marathe
44be2b99d2 Fix collapsed menu after reload on state closed 2024-12-12 16:55:40 +05:30
Anthony Stirling
549824c91f Merge pull request #2438 from Stirling-Tools/sync_readme
📝 Update README: Translation Progress Table
2024-12-12 11:08:31 +00:00
github-actions[bot]
1c0d1efda6 📝 Sync README
> Made via sync_files.yml
2024-12-12 11:01:38 +00:00
Anthony Stirling
9d8d90bf2f Merge pull request #2432 from Stirling-Tools/update_translation_files
Update translation files
2024-12-12 11:01:22 +00:00
snyk-bot
a7e7d57b23 fix: Dockerfile-ultra-lite to reduce vulnerabilities
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-ALPINE320-OPENSSL-8235201
- https://snyk.io/vuln/SNYK-ALPINE320-OPENSSL-8235201
2024-12-12 00:48:00 +00:00
Anthony Stirling
e014bb023b fixes 2024-12-11 23:21:56 +00:00
Anthony Stirling
1c5dfc46a0 fixes 2024-12-11 23:13:23 +00:00
Anthony Stirling
11273b1589 fix gha release 2024-12-11 22:29:58 +00:00
Anthony Stirling
dd5af46906 gha fix 2024-12-11 22:17:27 +00:00
Anthony Stirling
c20d37518d prop fixes 2024-12-11 22:09:35 +00:00
Anthony Stirling
eb20f51958 headless 2024-12-11 21:56:50 +00:00
Anthony Stirling
97d28ac6d2 Windows UI .exe 2024-12-11 21:54:05 +00:00
Anthony Stirling
026fe8150d Merge pull request #2427 from Stirling-Tools/testStuff
X-API-key to X-API-KEY and enable CSRF protection for all users
2024-12-11 21:52:57 +00:00
Ludy87
c3f88f716c Update GeneralUtils.java 2024-12-11 21:10:18 +01:00
Ludy87
67f983f00d Security fix: Server-Side Request Forgery
https://github.com/Stirling-Tools/Stirling-PDF/security/advisories/GHSA-4v4c-9hpr-93vx
2024-12-11 21:06:07 +01:00
github-actions[bot]
9167f12296 Update translation files
Signed-off-by: GitHub Action <action@github.com>
2024-12-11 17:28:28 +00:00
Anthony Stirling
93e190fdeb Merge pull request #2412 from reecebrowne/feature/1856/decrypt
Feature/1856/decrypt
2024-12-11 17:27:28 +00:00
Anthony Stirling
82bebf5c62 Merge branch 'main' into feature/1856/decrypt 2024-12-11 17:26:14 +00:00
Reece Browne
bb3f076e6d Final pages fixed for decryption 2024-12-11 16:55:27 +00:00
Reece Browne
64dfa4b841 Tweak additional files to integrate decryption and clean up js 2024-12-10 22:21:00 +00:00
thiagoor-cpu
0f6f3f305a Update messages_pt_BR.properties (#2429)
Up-to-date translation PT-BR
2024-12-10 20:40:28 +00:00
Anthony Stirling
58c7d7b9a8 X-API-key to X-API-KEY 2024-12-10 20:39:24 +00:00
Reece Browne
ef8231de3a Add Decrypt to all relevant pages 2024-12-10 16:39:06 +00:00
Anthony Stirling
c1c3eba398 ensure csrf is enabled 2024-12-10 11:17:50 +00:00
dependabot[bot]
52693541d9 Bump org.thymeleaf.extras:thymeleaf-extras-springsecurity5
Bumps org.thymeleaf.extras:thymeleaf-extras-springsecurity5 from 3.1.2.RELEASE to 3.1.3.RELEASE.

---
updated-dependencies:
- dependency-name: org.thymeleaf.extras:thymeleaf-extras-springsecurity5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-09 22:44:23 +00:00
Anthony Stirling
1639e0fc4c format 2024-12-09 20:41:13 +00:00
Anthony Stirling
0652299bec fixes 2024-12-09 20:40:59 +00:00
Reece Browne
1d6511b043 Check if file is encrypted without password 2024-12-09 13:20:08 +00:00
F43Z
a400fe6015 feat: fa locale added (#2416)
* feat: fa locale added

* fix: fa_locale issues fixed

* Update settings.json
2024-12-08 20:19:52 +00:00
Sai Kumar
b47df3d252 Text color selection for watermark (#2415)
* added custom color selection for Watermark

* using the same translation as AddStampRequest.customColor for the new watermark.customColor

* fixed the space issue between words
2024-12-07 14:19:50 +00:00
Michael Bohn
cb6e1cd94e Update messages_de_DE.properties (#2410)
Some minor changes/fixes to german translation.
2024-12-07 12:15:52 +00:00
Reece Browne
6ee6254f5a Additional decryption translations 2024-12-06 21:26:28 +00:00
Reece Browne
f2c9549ba1 Password prompt translations 2024-12-06 20:53:16 +00:00
Reece Browne
58278c07ff Translations for errors 2024-12-06 20:46:04 +00:00
Reece Browne
4d017610b8 PDF decryption 2024-12-06 19:08:18 +00:00
Reece Browne
dcafc0d487 Merge branch 'decrypt' of https://github.com/Stirling-Tools/Stirling-PDF 2024-12-06 15:33:44 +00:00
github-actions[bot]
2ec8c97737 📝 Update README: Translation Progress Table (#2409)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-12-06 12:27:14 +00:00
Anthony Stirling
397a07afe8 english translation fixes (#2408)
* english

* Update messages_en_GB.properties
2024-12-06 12:25:08 +00:00
github-actions[bot]
b072c39fd9 📝 Update README: Translation Progress Table (#2406)
📝 Sync README
> Made via sync_files.yml

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

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

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

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

* cert info

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

* Protect `readLine()` against DoS

* Switch order of literals to prevent NullPointerException

---------

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

* some basic html excaping and translation fixing

---------

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

* Add pages undo capability

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

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

* Fix form binding for dynamic Map entries in Change Metadata

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

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

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

 - Add separate drag and drop area for file choosers

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

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

* Update file chooser UI

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

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

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

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

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

* Use input element to dispatch event on file removal

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

* Adapt file chooser UI to themes

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

* Remove extra overlay border

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

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

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

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

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

* Fix merge file removal not being reflected in file chooser

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

* Make inputElement optional in removeFileById

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

* Add translation support to file chooser

---------

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

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

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

- Fix a bug that allowed multiple files to be dropped onto a single-file input element by accepting only the first file.
2024-11-29 17:31:14 +00:00
Anthony Stirling
f4082e3f96 Update PasswordController.java 2024-07-07 22:51:59 +01:00
Anthony Stirling
c93a48b40d Merge branch 'main' into decrypt 2024-07-07 22:50:41 +01:00
Anthony Stirling
75e10efcbd auto decrypt, update discord, fix multi file support for some inputs 2024-07-07 22:49:21 +01:00
188 changed files with 35658 additions and 3247 deletions

View File

@@ -1,5 +1,5 @@
blank_issues_enabled: true blank_issues_enabled: true
contact_links: contact_links:
- name: 💬 Discord Server - name: 💬 Discord Server
url: https://discord.gg/Cn8pWhQRxZ url: https://discord.gg/HYmhKj45pU
about: You can join our Discord server for real time discussion and support about: You can join our Discord server for real time discussion and support

View File

@@ -76,7 +76,7 @@ jobs:
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
python-version: "3.7" python-version: "3.12"
- name: Pip requirements - name: Pip requirements
run: | run: |

96
.github/workflows/multiOSReleases.yml vendored Normal file
View File

@@ -0,0 +1,96 @@
name: Test Installers Build
on:
workflow_dispatch:
release:
types: [created]
permissions:
contents: write
packages: write
jobs:
build-installers:
strategy:
matrix:
include:
- os: windows-latest
platform: win
ext: exe
#- os: macos-latest
# platform: mac
# ext: dmg
#- os: ubuntu-latest
# platform: linux
# ext: deb
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: "21"
distribution: "temurin"
- uses: gradle/actions/setup-gradle@v4
with:
gradle-version: 8.7
# Install Windows dependencies
- name: Install WiX Toolset
if: matrix.os == 'windows-latest'
run: |
curl -L -o wix.exe https://github.com/wixtoolset/wix3/releases/download/wix3141rtm/wix314.exe
.\wix.exe /install /quiet
# Install Linux dependencies
- name: Install Linux Dependencies
if: matrix.os == 'ubuntu-latest'
run: |
sudo apt-get update
sudo apt-get install -y fakeroot rpm
# Get version number
- name: Get version number
id: versionNumber
run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT
shell: bash
- name: Get version number mac
id: versionNumberMac
run: echo "versionNumberMac=$(./gradlew printMacVersion --quiet | tail -1)" >> $GITHUB_OUTPUT
shell: bash
# Build installer
- name: Build Installer
run: ./gradlew build jpackage -x test --info
env:
DOCKER_ENABLE_SECURITY: false
STIRLING_PDF_DESKTOP_UI: true
# Rename and collect artifacts based on OS
- name: Prepare artifacts
id: prepare
shell: bash
run: |
if [ "${{ matrix.os }}" = "windows-latest" ]; then
mv "build/jpackage/Stirling-PDF-${{ steps.versionNumber.outputs.versionNumber }}.exe" "Stirling-PDF-${{ matrix.platform }}-installer.${{ matrix.ext }}"
elif [ "${{ matrix.os }}" = "macos-latest" ]; then
mv "build/jpackage/Stirling-PDF-${{ steps.versionNumberMac.outputs.versionNumberMac }}.dmg" "Stirling-PDF-${{ steps.versionNumber.outputs.versionNumber }}-${{ matrix.platform }}.${{ matrix.ext }}"
else
mv "build/jpackage/stirling-pdf_${{ steps.versionNumber.outputs.versionNumber }}-1_amd64.deb" "Stirling-PDF-${{ steps.versionNumber.outputs.versionNumber }}-${{ matrix.platform }}.${{ matrix.ext }}"
fi
# Upload installer as artifact for testing
- name: Upload Installer Artifact
uses: actions/upload-artifact@v4
with:
name: Stirling-PDF-${{ matrix.platform }}-installer.${{ matrix.ext }}
path: Stirling-PDF-${{ matrix.platform }}-installer.${{ matrix.ext }}
retention-days: 1
if-no-files-found: error
- name: Upload binaries to release
uses: softprops/action-gh-release@v2
with:
files: ./Stirling-PDF-${{ matrix.platform }}-installer.${{ matrix.ext }}

View File

@@ -35,27 +35,28 @@ jobs:
run: ./gradlew clean createExe run: ./gradlew clean createExe
env: env:
DOCKER_ENABLE_SECURITY: ${{ matrix.enable_security }} DOCKER_ENABLE_SECURITY: ${{ matrix.enable_security }}
STIRLING_PDF_DESKTOP_UI: false
- name: Get version number - name: Get version number
id: versionNumber id: versionNumber
run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT
- name: Rename binarie - name: Rename binarie
if: matrix.file_suffix != '' run: cp ./build/launch4j/Stirling-PDF.exe ./build/launch4j/Stirling-PDF-Server${{ matrix.file_suffix }}.exe
run: cp ./build/launch4j/Stirling-PDF.exe ./build/launch4j/Stirling-PDF${{ matrix.file_suffix }}.exe
- name: Upload Assets binarie - name: Upload Assets binarie
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
path: ./build/launch4j/Stirling-PDF${{ matrix.file_suffix }}.exe path: ./build/launch4j/Stirling-PDF-Server${{ matrix.file_suffix }}.exe
name: Stirling-PDF${{ matrix.file_suffix }}.exe name: Stirling-PDF-Server${{ matrix.file_suffix }}.exe
overwrite: true overwrite: true
retention-days: 1 retention-days: 1
if-no-files-found: error if-no-files-found: error
- name: Upload binaries to release - name: Upload binaries to release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
files: ./build/launch4j/Stirling-PDF${{ matrix.file_suffix }}.exe files: ./build/launch4j/Stirling-PDF-Server${{ matrix.file_suffix }}.exe
- name: Rename jar binaries - name: Rename jar binaries
run: cp ./build/libs/Stirling-PDF-${{ steps.versionNumber.outputs.versionNumber }}.jar ./build/libs/Stirling-PDF${{ matrix.file_suffix }}.jar run: cp ./build/libs/Stirling-PDF-${{ steps.versionNumber.outputs.versionNumber }}.jar ./build/libs/Stirling-PDF${{ matrix.file_suffix }}.jar

1
.gitignore vendored
View File

@@ -161,3 +161,4 @@ out/
.pytest_cache .pytest_cache
.ipynb_checkpoints .ipynb_checkpoints
**/jcef-bundle/

View File

@@ -49,5 +49,7 @@
"editor.indentSize": "tabSize", "editor.indentSize": "tabSize",
"editor.stickyScroll.enabled": false, "editor.stickyScroll.enabled": false,
"editor.minimap.enabled": false, "editor.minimap.enabled": false,
"editor.formatOnSave": true "editor.formatOnSave": true,
"java.format.settings.google.mode": "jar-file",
"java.format.settings.google.extra": "--aosp --skip-sorting-imports"
} }

View File

@@ -1,12 +1,5 @@
# New Database Backup and Import Functionality # New Database Backup and Import Functionality
> [!IMPORTANT]
> **Full activation will take place on approximately January 5th, 2025!**
Why is the waiting time six months?
There are users who only install updates sporadically; if they skip the preparation, it can/will lead to data loss in the database.
## Functionality Overview ## Functionality Overview
The newly introduced feature enhances the application with robust database backup and import capabilities. This feature is designed to ensure data integrity and provide a straightforward way to manage database backups. Here's how it works: The newly introduced feature enhances the application with robust database backup and import capabilities. This feature is designed to ensure data integrity and provide a straightforward way to manage database backups. Here's how it works:

View File

@@ -1,5 +1,5 @@
# use alpine # use alpine
FROM alpine:3.20.3 FROM alpine:3.21.0
ARG VERSION_TAG ARG VERSION_TAG

View File

@@ -2,7 +2,7 @@
<h1 align="center">Stirling-PDF</h1> <h1 align="center">Stirling-PDF</h1>
[![Docker Pulls](https://img.shields.io/docker/pulls/frooodle/s-pdf)](https://hub.docker.com/r/frooodle/s-pdf) [![Docker Pulls](https://img.shields.io/docker/pulls/frooodle/s-pdf)](https://hub.docker.com/r/frooodle/s-pdf)
[![Discord](https://img.shields.io/discord/1068636748814483718?label=Discord)](https://discord.gg/Cn8pWhQRxZ) [![Discord](https://img.shields.io/discord/1068636748814483718?label=Discord)](https://discord.gg/HYmhKj45pU)
[![Docker Image Version (tag latest semver)](https://img.shields.io/docker/v/frooodle/s-pdf/latest)](https://github.com/Stirling-Tools/Stirling-PDF/) [![Docker Image Version (tag latest semver)](https://img.shields.io/docker/v/frooodle/s-pdf/latest)](https://github.com/Stirling-Tools/Stirling-PDF/)
[![GitHub Repo stars](https://img.shields.io/github/stars/stirling-tools/stirling-pdf?style=social)](https://github.com/Stirling-Tools/stirling-pdf) [![GitHub Repo stars](https://img.shields.io/github/stars/stirling-tools/stirling-pdf?style=social)](https://github.com/Stirling-Tools/stirling-pdf)
@@ -19,7 +19,7 @@ All files and PDFs exist either exclusively on the client side, reside in server
## Features ## Features
- Enterprise features like SSO Check [here](https://docs.stirlingpdf.com/Enterprise%20Edition) - Enterprise features like SSO Check [here](https://docs.stirlingpdf.com/Enterprise%20Edition)
- Dark mode support - Dark mode support
- Custom download options - Custom download options
- Parallel file processing and downloads - Parallel file processing and downloads
@@ -187,47 +187,48 @@ Certain functionality like `Sign` supports pre-saved files stored at `/customFil
## Supported Languages ## Supported Languages
Stirling-PDF currently supports 37 languages! Stirling-PDF currently supports 38 languages!
| Language | Progress | | Language | Progress |
| -------------------------------------------- | -------------------------------------- | | -------------------------------------------- | -------------------------------------- |
| Arabic (العربية) (ar_AR) | ![98%](https://geps.dev/progress/98) | | Arabic (العربية) (ar_AR) | ![94%](https://geps.dev/progress/94) |
| Azerbaijani (Azərbaycan Dili) (az_AZ) | ![97%](https://geps.dev/progress/97) | | Azerbaijani (Azərbaycan Dili) (az_AZ) | ![92%](https://geps.dev/progress/92) |
| Basque (Euskara) (eu_ES) | ![54%](https://geps.dev/progress/54) | | Basque (Euskara) (eu_ES) | ![53%](https://geps.dev/progress/53) |
| Bulgarian (Български) (bg_BG) | ![94%](https://geps.dev/progress/94) | | Bulgarian (Български) (bg_BG) | ![89%](https://geps.dev/progress/89) |
| Catalan (Català) (ca_CA) | ![88%](https://geps.dev/progress/88) | | Catalan (Català) (ca_CA) | ![84%](https://geps.dev/progress/84) |
| Croatian (Hrvatski) (hr_HR) | ![96%](https://geps.dev/progress/96) | | Croatian (Hrvatski) (hr_HR) | ![91%](https://geps.dev/progress/91) |
| Czech (Česky) (cs_CZ) | ![95%](https://geps.dev/progress/95) | | Czech (Česky) (cs_CZ) | ![90%](https://geps.dev/progress/90) |
| Danish (Dansk) (da_DK) | ![94%](https://geps.dev/progress/94) | | Danish (Dansk) (da_DK) | ![89%](https://geps.dev/progress/89) |
| Dutch (Nederlands) (nl_NL) | ![94%](https://geps.dev/progress/94) | | Dutch (Nederlands) (nl_NL) | ![89%](https://geps.dev/progress/89) |
| 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) |
| French (Français) (fr_FR) | ![97%](https://geps.dev/progress/97) | | French (Français) (fr_FR) | ![92%](https://geps.dev/progress/92) |
| German (Deutsch) (de_DE) | ![97%](https://geps.dev/progress/97) | | German (Deutsch) (de_DE) | ![100%](https://geps.dev/progress/100) |
| Greek (Ελληνικά) (el_GR) | ![95%](https://geps.dev/progress/95) | | Greek (Ελληνικά) (el_GR) | ![90%](https://geps.dev/progress/90) |
| Hindi (हिंदी) (hi_IN) | ![93%](https://geps.dev/progress/93) | | Hindi (हिंदी) (hi_IN) | ![88%](https://geps.dev/progress/88) |
| Hungarian (Magyar) (hu_HU) | ![95%](https://geps.dev/progress/95) | | Hungarian (Magyar) (hu_HU) | ![91%](https://geps.dev/progress/91) |
| Indonesian (Bahasa Indonesia) (id_ID) | ![95%](https://geps.dev/progress/95) | | Indonesian (Bahasa Indonesia) (id_ID) | ![90%](https://geps.dev/progress/90) |
| Irish (Gaeilge) (ga_IE) | ![86%](https://geps.dev/progress/86) | | Irish (Gaeilge) (ga_IE) | ![82%](https://geps.dev/progress/82) |
| Italian (Italiano) (it_IT) | ![99%](https://geps.dev/progress/99) | | Italian (Italiano) (it_IT) | ![99%](https://geps.dev/progress/99) |
| Japanese (日本語) (ja_JP) | ![83%](https://geps.dev/progress/83) | | Japanese (日本語) (ja_JP) | ![93%](https://geps.dev/progress/93) |
| Korean (한국어) (ko_KR) | ![93%](https://geps.dev/progress/93) | | Korean (한국어) (ko_KR) | ![89%](https://geps.dev/progress/89) |
| Norwegian (Norsk) (no_NB) | ![85%](https://geps.dev/progress/85) | | Norwegian (Norsk) (no_NB) | ![82%](https://geps.dev/progress/82) |
| Polish (Polski) (pl_PL) | ![94%](https://geps.dev/progress/94) | | Persian (فارسی) (fa_IR) | ![99%](https://geps.dev/progress/99) |
| Portuguese (Português) (pt_PT) | ![95%](https://geps.dev/progress/95) | | Polish (Polski) (pl_PL) | ![90%](https://geps.dev/progress/90) |
| Portuguese Brazilian (Português) (pt_BR) | ![96%](https://geps.dev/progress/96) | | Portuguese (Português) (pt_PT) | ![90%](https://geps.dev/progress/90) |
| Romanian (Română) (ro_RO) | ![88%](https://geps.dev/progress/88) | | Portuguese Brazilian (Português) (pt_BR) | ![98%](https://geps.dev/progress/98) |
| Russian (Русский) (ru_RU) | ![95%](https://geps.dev/progress/95) | | Romanian (Română) (ro_RO) | ![84%](https://geps.dev/progress/84) |
| Serbian Latin alphabet (Srpski) (sr_LATN_RS) | ![68%](https://geps.dev/progress/68) | | Russian (Русский) (ru_RU) | ![90%](https://geps.dev/progress/90) |
| Simplified Chinese (简体中文) (zh_CN) | ![89%](https://geps.dev/progress/89) | | Serbian Latin alphabet (Srpski) (sr_LATN_RS) | ![67%](https://geps.dev/progress/67) |
| Slovakian (Slovensky) (sk_SK) | ![80%](https://geps.dev/progress/80) | | Simplified Chinese (简体中文) (zh_CN) | ![93%](https://geps.dev/progress/93) |
| Spanish (Español) (es_ES) | ![96%](https://geps.dev/progress/96) | | Slovakian (Slovensky) (sk_SK) | ![78%](https://geps.dev/progress/78) |
| Swedish (Svenska) (sv_SE) | ![94%](https://geps.dev/progress/94) | | Spanish (Español) (es_ES) | ![91%](https://geps.dev/progress/91) |
| Thai (ไทย) (th_TH) | ![94%](https://geps.dev/progress/94) | | Swedish (Svenska) (sv_SE) | ![90%](https://geps.dev/progress/90) |
| Traditional Chinese (繁體中文) (zh_TW) | ![96%](https://geps.dev/progress/96) | | Thai (ไทย) (th_TH) | ![90%](https://geps.dev/progress/90) |
| Turkish (Türkçe) (tr_TR) | ![90%](https://geps.dev/progress/90) | | Traditional Chinese (繁體中文) (zh_TW) | ![91%](https://geps.dev/progress/91) |
| Ukrainian (Українська) (uk_UA) | ![78%](https://geps.dev/progress/78) | | Turkish (Türkçe) (tr_TR) | ![86%](https://geps.dev/progress/86) |
| Vietnamese (Tiếng Việt) (vi_VN) | ![86%](https://geps.dev/progress/86) | | Ukrainian (Українська) (uk_UA) | ![76%](https://geps.dev/progress/76) |
| Vietnamese (Tiếng Việt) (vi_VN) | ![83%](https://geps.dev/progress/83) |
## Contributing (Creating Issues, Translations, Fixing Bugs, etc.) ## Contributing (Creating Issues, Translations, Fixing Bugs, etc.)
@@ -240,7 +241,7 @@ Stirling PDF offers a Enterprise edition of its software, This is the same great
### Whats included ### Whats included
- Prioritised Support tickets via support@stirlingpdf.com to reach directly to Stirling-PDF team for support and 1:1 meetings where applicable (Provided they come from same email domain registered with us) - Prioritised Support tickets via support@stirlingpdf.com to reach directly to Stirling-PDF team for support and 1:1 meetings where applicable (Provided they come from same email domain registered with us)
- Prioritised Enhancements to Stirling-PDF where applicable - Prioritised Enhancements to Stirling-PDF where applicable
- Base SSO support - Base SSO support
- Advanced SSO such as automated login handling (Coming very soon) - Advanced SSO such as automated login handling (Coming very soon)
- SAML SSO (Coming very soon) - SAML SSO (Coming very soon)
@@ -404,7 +405,7 @@ To access your account settings, go to Account Settings in the settings cog menu
To add new users, go to the bottom of Account Settings and hit 'Admin Settings'. Here you can add new users. The different roles mentioned within this are for rate limiting. This is a work in progress and will be expanded on more in the future. To add new users, go to the bottom of Account Settings and hit 'Admin Settings'. Here you can add new users. The different roles mentioned within this are for rate limiting. This is a work in progress and will be expanded on more in the future.
For API usage, you must provide a header with `X-API-Key` and the associated API key for that user. For API usage, you must provide a header with `X-API-KEY` and the associated API key for that user.
## FAQ ## FAQ

View File

@@ -8,6 +8,7 @@ plugins {
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.9" id "com.github.jk1.dependency-license-report" version "2.9"
//id "nebula.lint" version "19.0.3" //id "nebula.lint" version "19.0.3"
id("org.panteleyev.jpackageplugin") version "1.6.0"
} }
@@ -21,12 +22,12 @@ ext {
imageioVersion = "3.12.0" imageioVersion = "3.12.0"
lombokVersion = "1.18.36" lombokVersion = "1.18.36"
bouncycastleVersion = "1.79" bouncycastleVersion = "1.79"
springSecuritySamlVersion = "6.4.1" springSecuritySamlVersion = "6.4.2"
openSamlVersion = "4.3.2" openSamlVersion = "4.3.2"
} }
group = "stirling.software" group = "stirling.software"
version = "0.35.0" version = "0.36.5"
java { java {
@@ -41,6 +42,9 @@ repositories {
maven { maven {
url 'https://build.shibboleth.net/maven/releases' url 'https://build.shibboleth.net/maven/releases'
} }
maven { url "https://build.shibboleth.net/maven/releases" }
maven { url "https://maven.pkg.github.com/jcefmaven/jcefmaven" }
} }
licenseReport { licenseReport {
@@ -64,6 +68,12 @@ sourceSets {
exclude "stirling/software/SPDF/model/User.java" exclude "stirling/software/SPDF/model/User.java"
exclude "stirling/software/SPDF/repository/**" exclude "stirling/software/SPDF/repository/**"
} }
if (System.getenv("STIRLING_PDF_DESKTOP_UI") == "false") {
exclude "stirling/software/SPDF/UI/impl/**"
}
} }
} }
} }
@@ -74,16 +84,153 @@ openApi {
outputFileName = "SwaggerDoc.json" outputFileName = "SwaggerDoc.json"
} }
//0.11.5 to 2024.11.5
def getMacVersion(String version) {
def currentYear = java.time.Year.now().getValue()
def versionParts = version.split("\\.", 2)
return "${currentYear}.${versionParts.length > 1 ? versionParts[1] : versionParts[0]}"
}
jpackage {
input = "build/libs"
appName = "Stirling-PDF"
appVersion = project.version
vendor = "Stirling-Software"
appDescription = "Stirling PDF - Your Local PDF Editor"
mainJar = "Stirling-PDF-${project.version}.jar"
mainClass = "org.springframework.boot.loader.launch.JarLauncher"
icon = "src/main/resources/static/favicon.ico"
// JVM Options
javaOptions = [
"-DBROWSER_OPEN=true",
"-DSTIRLING_PDF_DESKTOP_UI=true",
"-Djava.awt.headless=false",
"-Dapple.awt.UIElement=true",
"--add-opens", "java.base/java.lang=ALL-UNNAMED",
"--add-opens", "java.desktop/java.awt.event=ALL-UNNAMED",
"--add-opens", "java.desktop/sun.awt=ALL-UNNAMED"
]
verbose = true
destination = "${projectDir}/build/jpackage"
// Windows-specific configuration
windows {
launcherAsService = false
appVersion = project.version
winConsole = false
winDirChooser = true
winMenu = true
winShortcut = true
winPerUserInstall = true
winMenuGroup = "Stirling Software"
winUpgradeUuid = "2a43ed0c-b8c2-40cf-89e1-751129b87641" // Unique identifier for updates
winHelpUrl = "https://github.com/Stirling-Tools/Stirling-PDF"
winUpdateUrl = "https://github.com/Stirling-Tools/Stirling-PDF/releases"
type = "exe"
installDir = "C:/Program Files/Stirling-PDF"
}
// macOS-specific configuration
mac {
appVersion = getMacVersion(project.version.toString())
icon = "src/main/resources/static/favicon.icns"
type = "dmg"
macPackageIdentifier = "com.stirling.software.pdf"
macPackageName = "Stirling-PDF"
macAppCategory = "public.app-category.productivity"
macSign = false // Enable signing
macAppStore = false // Not targeting App Store initially
//installDir = "Applications"
// Add license and other documentation to DMG
/*macDmgContent = [
"README.md",
"LICENSE",
"CHANGELOG.md"
]*/
// Enable Mac-specific entitlements
//macEntitlements = "entitlements.plist" // You'll need to create this file
}
// Linux-specific configuration
linux {
appVersion = project.version
icon = "src/main/resources/static/favicon.png"
type = "deb" // Can also use "rpm" for Red Hat-based systems
// Debian package configuration
//linuxPackageName = "stirlingpdf"
linuxDebMaintainer = "support@stirlingpdf.com"
linuxMenuGroup = "Office;PDF;Productivity"
linuxAppCategory = "Office"
linuxAppRelease = "1"
linuxPackageDeps = true
installDir = "/opt/Stirling-PDF"
// RPM-specific settings
//linuxRpmLicenseType = "MIT"
}
// Common additional options
//jLinkOptions = [
// "--strip-debug",
// "--compress=2",
// "--no-header-files",
// "--no-man-pages"
//]
// Add any additional modules required
/*addModules = [
"java.base",
"java.desktop",
"java.logging",
"java.sql",
"java.xml",
"jdk.crypto.ec"
]*/
// Add copyright and license information
copyright = "Copyright © 2024 Stirling Software"
licenseFile = "LICENSE"
}
launch4j { launch4j {
icon = "${projectDir}/src/main/resources/static/favicon.ico" icon = "${projectDir}/src/main/resources/static/favicon.ico"
outfile="Stirling-PDF.exe" outfile="Stirling-PDF.exe"
headerType="console"
if(System.getenv("STIRLING_PDF_DESKTOP_UI") == 'true') {
headerType = "gui"
} else {
headerType = "console"
}
jarTask = tasks.bootJar jarTask = tasks.bootJar
errTitle="Encountered error, Do you have Java 21?" errTitle="Encountered error, Do you have Java 21?"
downloadUrl="https://download.oracle.com/java/21/latest/jdk-21_windows-x64_bin.exe" downloadUrl="https://download.oracle.com/java/21/latest/jdk-21_windows-x64_bin.exe"
variables=["BROWSER_OPEN=true"]
if(System.getenv("STIRLING_PDF_DESKTOP_UI") == 'true') {
variables=["BROWSER_OPEN=true", "STIRLING_PDF_DESKTOP_UI=true"]
} else {
variables=["BROWSER_OPEN=true"]
}
jreMinVersion="17" jreMinVersion="17"
mutexName="Stirling-PDF" mutexName="Stirling-PDF"
@@ -100,7 +247,7 @@ spotless {
java { java {
target project.fileTree('src/main/java') target project.fileTree('src/main/java')
googleJavaFormat("1.22.0").aosp().reorderImports(false) googleJavaFormat("1.25.2").aosp().reorderImports(false)
importOrder("java", "javax", "org", "com", "net", "io") importOrder("java", "javax", "org", "com", "net", "io")
toggleOffOn() toggleOffOn()
@@ -123,10 +270,17 @@ configurations.all {
exclude group: "org.springframework.boot", module: "spring-boot-starter-tomcat" exclude group: "org.springframework.boot", module: "spring-boot-starter-tomcat"
} }
dependencies { dependencies {
//security updates
implementation "org.springframework:spring-webmvc:6.2.0"
implementation("io.github.pixee:java-security-toolkit:1.2.0") if (System.getenv("STIRLING_PDF_DESKTOP_UI") != "false") {
implementation "me.friwi:jcefmaven:127.3.1"
implementation "org.openjfx:javafx-controls:21"
implementation "org.openjfx:javafx-swing:21"
}
//security updates
implementation "org.springframework:spring-webmvc:6.2.1"
implementation("io.github.pixee:java-security-toolkit:1.2.1")
// 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'
@@ -142,12 +296,12 @@ dependencies {
if (System.getenv("DOCKER_ENABLE_SECURITY") != "false") { if (System.getenv("DOCKER_ENABLE_SECURITY") != "false") {
implementation "org.springframework.boot:spring-boot-starter-security:$springBootVersion" implementation "org.springframework.boot:spring-boot-starter-security:$springBootVersion"
implementation "org.thymeleaf.extras:thymeleaf-extras-springsecurity5:3.1.2.RELEASE" implementation "org.thymeleaf.extras:thymeleaf-extras-springsecurity5:3.1.3.RELEASE"
implementation "org.springframework.boot:spring-boot-starter-data-jpa:$springBootVersion" implementation "org.springframework.boot:spring-boot-starter-data-jpa:$springBootVersion"
implementation "org.springframework.boot:spring-boot-starter-oauth2-client:$springBootVersion" implementation "org.springframework.boot:spring-boot-starter-oauth2-client:$springBootVersion"
implementation "org.springframework.session:spring-session-core:$springBootVersion" implementation "org.springframework.session:spring-session-core:$springBootVersion"
implementation 'com.unboundid.product.scim2:scim2-sdk-client:2.3.5' implementation 'com.unboundid.product.scim2:scim2-sdk-client:2.3.5'
// Don't upgrade h2database // Don't upgrade h2database
runtimeOnly "com.h2database:h2:2.3.232" runtimeOnly "com.h2database:h2:2.3.232"
@@ -218,7 +372,7 @@ dependencies {
implementation "org.bouncycastle:bcprov-jdk18on:$bouncycastleVersion" implementation "org.bouncycastle:bcprov-jdk18on:$bouncycastleVersion"
implementation "org.bouncycastle:bcpkix-jdk18on:$bouncycastleVersion" 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.14.1" implementation "io.micrometer:micrometer-core:1.14.2"
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.24.0" implementation "org.commonmark:commonmark:0.24.0"
@@ -271,7 +425,14 @@ jar {
tasks.named("test") { tasks.named("test") {
useJUnitPlatform() useJUnitPlatform()
} }
task printVersion { task printVersion {
println project.version doLast {
println project.version
}
}
task printMacVersion {
doLast {
println getMacVersion(project.version.toString())
}
} }

View File

@@ -15,6 +15,10 @@ import shutil
import re import re
from PIL import Image, ImageDraw from PIL import Image, ImageDraw
API_HEADERS = {
'X-API-KEY': '123456789'
}
######### #########
# GIVEN # # GIVEN #
######### #########
@@ -227,7 +231,7 @@ def save_generated_pdf(context, filename):
def step_send_get_request(context, endpoint): def step_send_get_request(context, endpoint):
base_url = "http://localhost:8080" base_url = "http://localhost:8080"
full_url = f"{base_url}{endpoint}" full_url = f"{base_url}{endpoint}"
response = requests.get(full_url) response = requests.get(full_url, headers=API_HEADERS)
context.response = response context.response = response
@when('I send a GET request to "{endpoint}" with parameters') @when('I send a GET request to "{endpoint}" with parameters')
@@ -235,7 +239,7 @@ def step_send_get_request_with_params(context, endpoint):
base_url = "http://localhost:8080" base_url = "http://localhost:8080"
params = {row['parameter']: row['value'] for row in context.table} params = {row['parameter']: row['value'] for row in context.table}
full_url = f"{base_url}{endpoint}" full_url = f"{base_url}{endpoint}"
response = requests.get(full_url, params=params) response = requests.get(full_url, params=params, headers=API_HEADERS)
context.response = response context.response = response
@when('I send the API request to the endpoint "{endpoint}"') @when('I send the API request to the endpoint "{endpoint}"')
@@ -256,7 +260,7 @@ def step_send_api_request(context, endpoint):
print(f"form_data {file.name} with {mime_type}") print(f"form_data {file.name} with {mime_type}")
form_data.append((key, (file.name, file, mime_type))) form_data.append((key, (file.name, file, mime_type)))
response = requests.post(url, files=form_data) response = requests.post(url, files=form_data, headers=API_HEADERS)
context.response = response context.response = response
######## ########

View File

@@ -0,0 +1,34 @@
services:
stirling-pdf:
container_name: Stirling-PDF-Security-Fat
image: stirlingtools/stirling-pdf:latest-fat
deploy:
resources:
limits:
memory: 4G
healthcheck:
test: ["CMD-SHELL", "curl -f -H 'X-API-KEY: 123456789' http://localhost:8080/api/v1/info/status | grep -q 'UP'"]
interval: 5s
timeout: 10s
retries: 16
ports:
- 8080:8080
volumes:
- /stirling/latest/data:/usr/share/tessdata:rw
- /stirling/latest/config:/configs:rw
- /stirling/latest/logs:/logs:rw
environment:
DOCKER_ENABLE_SECURITY: "true"
SECURITY_ENABLELOGIN: "true"
PUID: 1002
PGID: 1002
UMASK: "022"
SYSTEM_DEFAULTLOCALE: en-US
UI_APPNAME: Stirling-PDF
UI_HOMEDESCRIPTION: Demo site for Stirling-PDF Latest-fat with Security
UI_APPNAMENAVBAR: Stirling-PDF Latest-fat
SYSTEM_MAXFILESIZE: "100"
METRICS_ENABLED: "true"
SYSTEM_GOOGLEVISIBILITY: "true"
SECURITY_CUSTOMGLOBALAPIKEY: "123456789"
restart: on-failure:5

View File

@@ -42,14 +42,19 @@ ignore = [
'addPageNumbers.selectText.3', 'addPageNumbers.selectText.3',
'alphabet', 'alphabet',
'certSign.name', 'certSign.name',
'fileChooser.dragAndDrop',
'home.pipeline.title', 'home.pipeline.title',
'language.direction', 'language.direction',
'legal.impressum',
'licenses.version', 'licenses.version',
'pipeline.title', 'pipeline.title',
'pipelineOptions.pipelineHeader', 'pipelineOptions.pipelineHeader',
'pro', 'pro',
'sponsor', 'sponsor',
'text', 'text',
'validateSignature.cert.bits',
'validateSignature.cert.version',
'validateSignature.status',
'watermark.type.1', 'watermark.type.1',
] ]
@@ -61,7 +66,6 @@ ignore = [
[es_ES] [es_ES]
ignore = [ ignore = [
'adminUserSettings.roles', 'adminUserSettings.roles',
'color',
'error', 'error',
'language.direction', 'language.direction',
'no', 'no',
@@ -73,6 +77,11 @@ ignore = [
'language.direction', 'language.direction',
] ]
[fa_IR]
ignore = [
'language.direction',
]
[fr_FR] [fr_FR]
ignore = [ ignore = [
'AddStampRequest.alphabet', 'AddStampRequest.alphabet',

View File

@@ -16,10 +16,10 @@ fi
# Check if TESSERACT_LANGS environment variable is set and is not empty # Check if TESSERACT_LANGS environment variable is set and is not empty
if [[ -n "$TESSERACT_LANGS" ]]; then if [[ -n "$TESSERACT_LANGS" ]]; then
# Convert comma-separated values to a space-separated list # Convert comma-separated values to a space-separated list
LANGS=$(echo $TESSERACT_LANGS | tr ',' ' ') SPACE_SEPARATED_LANGS=$(echo $TESSERACT_LANGS | tr ',' ' ')
pattern='^[a-zA-Z]{2,4}(_[a-zA-Z]{2,4})?$' pattern='^[a-zA-Z]{2,4}(_[a-zA-Z]{2,4})?$'
# Install each language pack # Install each language pack
for LANG in $LANGS; do for LANG in $SPACE_SEPARATED_LANGS; do
if [[ $LANG =~ $pattern ]]; then if [[ $LANG =~ $pattern ]]; then
apk add --no-cache "tesseract-ocr-data-$LANG" apk add --no-cache "tesseract-ocr-data-$LANG"
else else

View File

@@ -28,7 +28,7 @@ public class LicenseKeyChecker {
this.checkLicense(); this.checkLicense();
} }
@Scheduled(fixedRate = 604800000) // 7 days in milliseconds @Scheduled(initialDelay = 604800000, fixedRate = 604800000) // 7 days in milliseconds
public void checkLicensePeriodically() { public void checkLicensePeriodically() {
checkLicense(); checkLicense();
} }

View File

@@ -6,9 +6,6 @@ import java.net.Socket;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.github.pixee.security.SystemCommand; import io.github.pixee.security.SystemCommand;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -16,7 +13,6 @@ import lombok.extern.slf4j.Slf4j;
@Slf4j @Slf4j
public class LibreOfficeListener { public class LibreOfficeListener {
private static final Logger logger = LoggerFactory.getLogger(LibreOfficeListener.class);
private static final long ACTIVITY_TIMEOUT = 20L * 60 * 1000; // 20 minutes private static final long ACTIVITY_TIMEOUT = 20L * 60 * 1000; // 20 minutes
private static final LibreOfficeListener INSTANCE = new LibreOfficeListener(); private static final LibreOfficeListener INSTANCE = new LibreOfficeListener();
@@ -87,7 +83,7 @@ public class LibreOfficeListener {
Thread.sleep(1000); Thread.sleep(1000);
} catch (InterruptedException e) { } catch (InterruptedException e) {
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
logger.error("exception", e); log.error("exception", e);
} // Check every 1 second } // Check every 1 second
} }
} }

View File

@@ -1,5 +1,6 @@
package stirling.software.SPDF; package stirling.software.SPDF;
import java.awt.*;
import java.io.IOException; import java.io.IOException;
import java.net.ServerSocket; import java.net.ServerSocket;
import java.nio.file.Files; import java.nio.file.Files;
@@ -8,9 +9,10 @@ import java.nio.file.Paths;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Properties;
import javax.swing.*;
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.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
@@ -21,15 +23,17 @@ import org.springframework.scheduling.annotation.EnableScheduling;
import io.github.pixee.security.SystemCommand; import io.github.pixee.security.SystemCommand;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.UI.WebBrowser;
import stirling.software.SPDF.config.ConfigInitializer; import stirling.software.SPDF.config.ConfigInitializer;
import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.ApplicationProperties;
@SpringBootApplication @SpringBootApplication
@EnableScheduling @EnableScheduling
@Slf4j
public class SPdfApplication { public class SPdfApplication {
private static final Logger logger = LoggerFactory.getLogger(SPdfApplication.class);
@Autowired private Environment env; @Autowired private Environment env;
@Autowired ApplicationProperties applicationProperties; @Autowired ApplicationProperties applicationProperties;
@@ -67,36 +71,19 @@ public class SPdfApplication {
} }
} }
@PostConstruct
public void init() {
baseUrlStatic = this.baseUrl;
// Check if the BROWSER_OPEN environment variable is set to true
String browserOpenEnv = env.getProperty("BROWSER_OPEN");
boolean browserOpen = browserOpenEnv != null && "true".equalsIgnoreCase(browserOpenEnv);
if (browserOpen) {
try {
String url = baseUrl + ":" + getStaticPort();
String os = System.getProperty("os.name").toLowerCase();
Runtime rt = Runtime.getRuntime();
if (os.contains("win")) {
// For Windows
SystemCommand.runCommand(rt, "rundll32 url.dll,FileProtocolHandler " + url);
} else if (os.contains("mac")) {
SystemCommand.runCommand(rt, "open " + url);
} else if (os.contains("nix") || os.contains("nux")) {
SystemCommand.runCommand(rt, "xdg-open " + url);
}
} catch (Exception e) {
logger.error("Error opening browser: {}", e.getMessage());
}
}
logger.info("Running configs {}", applicationProperties.toString());
}
public static void main(String[] args) throws IOException, InterruptedException { public static void main(String[] args) throws IOException, InterruptedException {
SpringApplication app = new SpringApplication(SPdfApplication.class); SpringApplication app = new SpringApplication(SPdfApplication.class);
Properties props = new Properties();
if (Boolean.parseBoolean(System.getProperty("STIRLING_PDF_DESKTOP_UI", "false"))) {
System.setProperty("java.awt.headless", "false");
app.setHeadless(false);
props.put("java.awt.headless", "false");
props.put("spring.main.web-application-type", "servlet");
}
app.setAdditionalProfiles("default"); app.setAdditionalProfiles("default");
app.addInitializers(new ConfigInitializer()); app.addInitializers(new ConfigInitializer());
Map<String, String> propertyFiles = new HashMap<>(); Map<String, String> propertyFiles = new HashMap<>();
@@ -105,7 +92,7 @@ public class SPdfApplication {
if (Files.exists(Paths.get("configs/settings.yml"))) { if (Files.exists(Paths.get("configs/settings.yml"))) {
propertyFiles.put("spring.config.additional-location", "file:configs/settings.yml"); propertyFiles.put("spring.config.additional-location", "file:configs/settings.yml");
} else { } else {
logger.warn("External configuration file 'configs/settings.yml' does not exist."); log.warn("External configuration file 'configs/settings.yml' does not exist.");
} }
if (Files.exists(Paths.get("configs/custom_settings.yml"))) { if (Files.exists(Paths.get("configs/custom_settings.yml"))) {
@@ -118,16 +105,22 @@ public class SPdfApplication {
"spring.config.additional-location", "spring.config.additional-location",
existingLocation + "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."); log.warn("Custom configuration file 'configs/custom_settings.yml' does not exist.");
} }
Properties finalProps = new Properties();
if (!propertyFiles.isEmpty()) { if (!propertyFiles.isEmpty()) {
app.setDefaultProperties( finalProps.putAll(
Collections.singletonMap( Collections.singletonMap(
"spring.config.additional-location", "spring.config.additional-location",
propertyFiles.get("spring.config.additional-location"))); propertyFiles.get("spring.config.additional-location")));
} }
if (!props.isEmpty()) {
finalProps.putAll(props);
}
app.setDefaultProperties(finalProps);
app.run(args); app.run(args);
// Ensure directories are created // Ensure directories are created
@@ -135,16 +128,56 @@ public class SPdfApplication {
Files.createDirectories(Path.of("customFiles/static/")); Files.createDirectories(Path.of("customFiles/static/"));
Files.createDirectories(Path.of("customFiles/templates/")); Files.createDirectories(Path.of("customFiles/templates/"));
} catch (Exception e) { } catch (Exception e) {
logger.error("Error creating directories: {}", e.getMessage()); log.error("Error creating directories: {}", e.getMessage());
} }
printStartupLogs(); printStartupLogs();
} }
private static void printStartupLogs() { private static void printStartupLogs() {
logger.info("Stirling-PDF Started."); log.info("Stirling-PDF Started.");
String url = baseUrlStatic + ":" + getStaticPort(); String url = baseUrlStatic + ":" + getStaticPort();
logger.info("Navigate to {}", url); log.info("Navigate to {}", url);
}
@Autowired(required = false)
private WebBrowser webBrowser;
@PostConstruct
public void init() {
baseUrlStatic = this.baseUrl;
String url = baseUrl + ":" + getStaticPort();
if (webBrowser != null
&& Boolean.parseBoolean(System.getProperty("STIRLING_PDF_DESKTOP_UI", "false"))) {
webBrowser.initWebUI(url);
} else {
String browserOpenEnv = env.getProperty("BROWSER_OPEN");
boolean browserOpen = browserOpenEnv != null && "true".equalsIgnoreCase(browserOpenEnv);
if (browserOpen) {
try {
String os = System.getProperty("os.name").toLowerCase();
Runtime rt = Runtime.getRuntime();
if (os.contains("win")) {
// For Windows
SystemCommand.runCommand(rt, "rundll32 url.dll,FileProtocolHandler " + url);
} else if (os.contains("mac")) {
SystemCommand.runCommand(rt, "open " + url);
} else if (os.contains("nix") || os.contains("nux")) {
SystemCommand.runCommand(rt, "xdg-open " + url);
}
} catch (Exception e) {
log.error("Error opening browser: {}", e.getMessage());
}
}
}
log.info("Running configs {}", applicationProperties.toString());
}
@PreDestroy
public void cleanup() {
if (webBrowser != null) {
webBrowser.cleanup();
}
} }
public static String getStaticBaseUrl() { public static String getStaticBaseUrl() {

View File

@@ -0,0 +1,7 @@
package stirling.software.SPDF.UI;
public interface WebBrowser {
void initWebUI(String url);
void cleanup();
}

View File

@@ -0,0 +1,354 @@
package stirling.software.SPDF.UI.impl;
import java.awt.AWTException;
import java.awt.BorderLayout;
import java.awt.Frame;
import java.awt.Image;
import java.awt.MenuItem;
import java.awt.PopupMenu;
import java.awt.SystemTray;
import java.awt.TrayIcon;
import java.awt.event.WindowEvent;
import java.awt.event.WindowStateListener;
import java.io.File;
import java.io.InputStream;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import javax.imageio.ImageIO;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import org.cef.CefApp;
import org.cef.CefClient;
import org.cef.CefSettings;
import org.cef.browser.CefBrowser;
import org.cef.callback.CefBeforeDownloadCallback;
import org.cef.callback.CefDownloadItem;
import org.cef.callback.CefDownloadItemCallback;
import org.cef.handler.CefDownloadHandlerAdapter;
import org.cef.handler.CefLoadHandlerAdapter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
import jakarta.annotation.PreDestroy;
import lombok.extern.slf4j.Slf4j;
import me.friwi.jcefmaven.CefAppBuilder;
import me.friwi.jcefmaven.EnumProgress;
import me.friwi.jcefmaven.MavenCefAppHandlerAdapter;
import me.friwi.jcefmaven.impl.progress.ConsoleProgressHandler;
import stirling.software.SPDF.UI.WebBrowser;
@Component
@Slf4j
@ConditionalOnProperty(
name = "STIRLING_PDF_DESKTOP_UI",
havingValue = "true",
matchIfMissing = false)
public class DesktopBrowser implements WebBrowser {
private static CefApp cefApp;
private static CefClient client;
private static CefBrowser browser;
private static JFrame frame;
private static LoadingWindow loadingWindow;
private static volatile boolean browserInitialized = false;
private static TrayIcon trayIcon;
private static SystemTray systemTray;
public DesktopBrowser() {
SwingUtilities.invokeLater(
() -> {
loadingWindow = new LoadingWindow(null, "Initializing...");
loadingWindow.setVisible(true);
});
}
public void initWebUI(String url) {
CompletableFuture.runAsync(
() -> {
try {
CefAppBuilder builder = new CefAppBuilder();
configureCefSettings(builder);
builder.setProgressHandler(createProgressHandler());
// Build and initialize CEF
cefApp = builder.build();
client = cefApp.createClient();
// Set up download handler
setupDownloadHandler();
// Create browser and frame on EDT
SwingUtilities.invokeAndWait(
() -> {
browser = client.createBrowser(url, false, false);
setupMainFrame();
setupLoadHandler();
// Show the frame immediately but transparent
frame.setVisible(true);
});
} catch (Exception e) {
log.error("Error initializing JCEF browser: ", e);
cleanup();
}
});
}
private void configureCefSettings(CefAppBuilder builder) {
CefSettings settings = builder.getCefSettings();
settings.cache_path = new File("jcef-bundle").getAbsolutePath();
settings.root_cache_path = new File("jcef-bundle").getAbsolutePath();
settings.persist_session_cookies = true;
settings.windowless_rendering_enabled = false;
settings.log_severity = CefSettings.LogSeverity.LOGSEVERITY_INFO;
builder.setAppHandler(
new MavenCefAppHandlerAdapter() {
@Override
public void stateHasChanged(org.cef.CefApp.CefAppState state) {
log.info("CEF state changed: " + state);
if (state == CefApp.CefAppState.TERMINATED) {
System.exit(0);
}
}
});
}
private void setupDownloadHandler() {
client.addDownloadHandler(
new CefDownloadHandlerAdapter() {
@Override
public boolean onBeforeDownload(
CefBrowser browser,
CefDownloadItem downloadItem,
String suggestedName,
CefBeforeDownloadCallback callback) {
callback.Continue("", true);
return true;
}
@Override
public void onDownloadUpdated(
CefBrowser browser,
CefDownloadItem downloadItem,
CefDownloadItemCallback callback) {
if (downloadItem.isComplete()) {
log.info("Download completed: " + downloadItem.getFullPath());
} else if (downloadItem.isCanceled()) {
log.info("Download canceled: " + downloadItem.getFullPath());
}
}
});
}
private ConsoleProgressHandler createProgressHandler() {
return new ConsoleProgressHandler() {
@Override
public void handleProgress(EnumProgress state, float percent) {
Objects.requireNonNull(state, "state cannot be null");
SwingUtilities.invokeLater(
() -> {
if (loadingWindow != null) {
switch (state) {
case LOCATING:
loadingWindow.setStatus("Locating Files...");
loadingWindow.setProgress(0);
break;
case DOWNLOADING:
if (percent >= 0) {
loadingWindow.setStatus(
String.format(
"Downloading additional files: %.0f%%",
percent));
loadingWindow.setProgress((int) percent);
}
break;
case EXTRACTING:
loadingWindow.setStatus("Extracting files...");
loadingWindow.setProgress(60);
break;
case INITIALIZING:
loadingWindow.setStatus("Initializing UI...");
loadingWindow.setProgress(80);
break;
case INITIALIZED:
loadingWindow.setStatus("Finalising startup...");
loadingWindow.setProgress(90);
break;
}
}
});
}
};
}
private void setupMainFrame() {
frame = new JFrame("Stirling-PDF");
frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
frame.setUndecorated(true);
frame.setOpacity(0.0f);
JPanel contentPane = new JPanel(new BorderLayout());
contentPane.setDoubleBuffered(true);
contentPane.add(browser.getUIComponent(), BorderLayout.CENTER);
frame.setContentPane(contentPane);
frame.addWindowListener(
new java.awt.event.WindowAdapter() {
@Override
public void windowClosing(java.awt.event.WindowEvent windowEvent) {
cleanup();
System.exit(0);
}
});
frame.setSize(1280, 768);
frame.setLocationRelativeTo(null);
loadIcon();
}
private void setupLoadHandler() {
client.addLoadHandler(
new CefLoadHandlerAdapter() {
@Override
public void onLoadingStateChange(
CefBrowser browser,
boolean isLoading,
boolean canGoBack,
boolean canGoForward) {
if (!isLoading && !browserInitialized) {
browserInitialized = true;
SwingUtilities.invokeLater(
() -> {
if (loadingWindow != null) {
Timer timer =
new Timer(
500,
e -> {
loadingWindow.dispose();
loadingWindow = null;
frame.dispose();
frame.setOpacity(1.0f);
frame.setUndecorated(false);
frame.pack();
frame.setSize(1280, 800);
frame.setLocationRelativeTo(null);
frame.setVisible(true);
frame.requestFocus();
frame.toFront();
browser.getUIComponent()
.requestFocus();
});
timer.setRepeats(false);
timer.start();
}
});
}
}
});
}
private void setupTrayIcon(Image icon) {
if (!SystemTray.isSupported()) {
log.warn("System tray is not supported");
return;
}
try {
systemTray = SystemTray.getSystemTray();
// Create popup menu
PopupMenu popup = new PopupMenu();
// Create menu items
MenuItem showItem = new MenuItem("Show");
showItem.addActionListener(
e -> {
frame.setVisible(true);
frame.setState(Frame.NORMAL);
});
MenuItem exitItem = new MenuItem("Exit");
exitItem.addActionListener(
e -> {
cleanup();
System.exit(0);
});
// Add menu items to popup menu
popup.add(showItem);
popup.addSeparator();
popup.add(exitItem);
// Create tray icon
trayIcon = new TrayIcon(icon, "Stirling-PDF", popup);
trayIcon.setImageAutoSize(true);
// Add double-click behavior
trayIcon.addActionListener(
e -> {
frame.setVisible(true);
frame.setState(Frame.NORMAL);
});
// Add tray icon to system tray
systemTray.add(trayIcon);
// Modify frame behavior to minimize to tray
frame.addWindowStateListener(
new WindowStateListener() {
public void windowStateChanged(WindowEvent e) {
if (e.getNewState() == Frame.ICONIFIED) {
frame.setVisible(false);
}
}
});
} catch (AWTException e) {
log.error("Error setting up system tray icon", e);
}
}
private void loadIcon() {
try {
Image icon = null;
String[] iconPaths = {"/static/favicon.ico"};
for (String path : iconPaths) {
if (icon != null) break;
try {
try (InputStream is = getClass().getResourceAsStream(path)) {
if (is != null) {
icon = ImageIO.read(is);
break;
}
}
} catch (Exception e) {
log.debug("Could not load icon from " + path, e);
}
}
if (icon != null) {
frame.setIconImage(icon);
setupTrayIcon(icon);
} else {
log.warn("Could not load icon from any source");
}
} catch (Exception e) {
log.error("Error loading icon", e);
}
}
@PreDestroy
public void cleanup() {
if (browser != null) browser.close(true);
if (client != null) client.dispose();
if (cefApp != null) cefApp.dispose();
if (loadingWindow != null) loadingWindow.dispose();
}
}

View File

@@ -0,0 +1,114 @@
package stirling.software.SPDF.UI.impl;
import java.awt.*;
import java.io.InputStream;
import javax.imageio.ImageIO;
import javax.swing.*;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class LoadingWindow extends JDialog {
private final JProgressBar progressBar;
private final JLabel statusLabel;
private final JPanel mainPanel;
private final JLabel brandLabel;
public LoadingWindow(Frame parent, String initialUrl) {
super(parent, "Initializing Stirling-PDF", true);
// Initialize components
mainPanel = new JPanel();
mainPanel.setBackground(Color.WHITE);
mainPanel.setBorder(BorderFactory.createEmptyBorder(20, 30, 20, 30));
mainPanel.setLayout(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
// Configure GridBagConstraints
gbc.gridwidth = GridBagConstraints.REMAINDER;
gbc.fill = GridBagConstraints.HORIZONTAL;
gbc.insets = new Insets(5, 5, 5, 5);
gbc.weightx = 1.0; // Add horizontal weight
gbc.weighty = 0.0; // Add vertical weight
// Add icon
try {
try (InputStream is = getClass().getResourceAsStream("/static/favicon.ico")) {
if (is != null) {
Image img = ImageIO.read(is);
if (img != null) {
Image scaledImg = img.getScaledInstance(48, 48, Image.SCALE_SMOOTH);
JLabel iconLabel = new JLabel(new ImageIcon(scaledImg));
iconLabel.setHorizontalAlignment(SwingConstants.CENTER);
gbc.gridy = 0;
mainPanel.add(iconLabel, gbc);
}
}
}
} catch (Exception e) {
log.error("Failed to load icon", e);
}
// URL Label with explicit size
brandLabel = new JLabel(initialUrl);
brandLabel.setHorizontalAlignment(SwingConstants.CENTER);
brandLabel.setPreferredSize(new Dimension(300, 25));
brandLabel.setText("Stirling-PDF");
gbc.gridy = 1;
mainPanel.add(brandLabel, gbc);
// Status label with explicit size
statusLabel = new JLabel("Initializing...");
statusLabel.setHorizontalAlignment(SwingConstants.CENTER);
statusLabel.setPreferredSize(new Dimension(300, 25));
gbc.gridy = 2;
mainPanel.add(statusLabel, gbc);
// Progress bar with explicit size
progressBar = new JProgressBar(0, 100);
progressBar.setStringPainted(true);
progressBar.setPreferredSize(new Dimension(300, 25));
gbc.gridy = 3;
mainPanel.add(progressBar, gbc);
// Set dialog properties
setContentPane(mainPanel);
setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE);
setResizable(false);
setUndecorated(false);
// Set size and position
setSize(400, 200);
setLocationRelativeTo(parent);
setAlwaysOnTop(true);
setProgress(0);
setStatus("Starting...");
}
public void setProgress(final int progress) {
SwingUtilities.invokeLater(
() -> {
try {
progressBar.setValue(Math.min(Math.max(progress, 0), 100));
progressBar.setString(progress + "%");
mainPanel.revalidate();
mainPanel.repaint();
} catch (Exception e) {
log.error("Error updating progress", e);
}
});
}
public void setStatus(final String status) {
log.info(status);
SwingUtilities.invokeLater(
() -> {
try {
statusLabel.setText(status != null ? status : "");
mainPanel.revalidate();
mainPanel.repaint();
} catch (Exception e) {
log.error("Error updating status", e);
}
});
}
}

View File

@@ -7,8 +7,6 @@ import java.nio.file.Paths;
import java.util.Properties; import java.util.Properties;
import java.util.function.Predicate; import java.util.function.Predicate;
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.boot.autoconfigure.condition.ConditionalOnMissingClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
@@ -21,14 +19,14 @@ import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.ResourceLoader;
import org.thymeleaf.spring6.SpringTemplateEngine; import org.thymeleaf.spring6.SpringTemplateEngine;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.ApplicationProperties;
@Configuration @Configuration
@Lazy @Lazy
@Slf4j
public class AppConfig { public class AppConfig {
private static final Logger logger = LoggerFactory.getLogger(AppConfig.class);
@Autowired ApplicationProperties applicationProperties; @Autowired ApplicationProperties applicationProperties;
@Bean @Bean
@@ -61,7 +59,7 @@ public class AppConfig {
props.load(resource.getInputStream()); props.load(resource.getInputStream());
return props.getProperty("version"); return props.getProperty("version");
} catch (IOException e) { } catch (IOException e) {
logger.error("exception", e); log.error("exception", e);
} }
return "0.0.0"; return "0.0.0";
} }
@@ -101,6 +99,27 @@ public class AppConfig {
return Files.exists(Paths.get("/.dockerenv")); return Files.exists(Paths.get("/.dockerenv"));
} }
@Bean(name = "configDirMounted")
public boolean isRunningInDockerWithConfig() {
Path dockerEnv = Paths.get("/.dockerenv");
// default to true if not docker
if (!Files.exists(dockerEnv)) {
return true;
}
Path mountInfo = Paths.get("/proc/1/mountinfo");
// this should always exist, if not some unknown usecase
if (!Files.exists(mountInfo)) {
return true;
}
try {
return Files.lines(mountInfo).anyMatch(line -> line.contains(" /configs "));
} catch (IOException e) {
return false;
}
}
@Bean(name = "bookAndHtmlFormatsInstalled") @Bean(name = "bookAndHtmlFormatsInstalled")
public boolean bookAndHtmlFormatsInstalled() { public boolean bookAndHtmlFormatsInstalled() {
String installOps = System.getProperty("INSTALL_BOOK_AND_ADVANCED_HTML_OPS"); String installOps = System.getProperty("INSTALL_BOOK_AND_ADVANCED_HTML_OPS");

View File

@@ -16,16 +16,15 @@ 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.SimpleYamlImplementation;
import org.simpleyaml.configuration.implementation.snakeyaml.lib.DumperOptions; import org.simpleyaml.configuration.implementation.snakeyaml.lib.DumperOptions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContextInitializer; import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.ConfigurableApplicationContext;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class ConfigInitializer public class ConfigInitializer
implements ApplicationContextInitializer<ConfigurableApplicationContext> { implements ApplicationContextInitializer<ConfigurableApplicationContext> {
private static final Logger logger = LoggerFactory.getLogger(ConfigInitializer.class);
@Override @Override
public void initialize(ConfigurableApplicationContext applicationContext) { public void initialize(ConfigurableApplicationContext applicationContext) {
try { try {
@@ -149,7 +148,7 @@ public class ConfigInitializer
.commentSide(settingsTemplateFile.getComment(path, CommentType.SIDE)); .commentSide(settingsTemplateFile.getComment(path, CommentType.SIDE));
} else { } else {
// Log if the key is not found in both YAML files // Log if the key is not found in both YAML files
logger.info("Key not found in both YAML files: " + path); log.info("Key not found in both YAML files: " + path);
} }
} }
} }

View File

@@ -7,19 +7,19 @@ import java.util.Set;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors; 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.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.DependsOn; import org.springframework.context.annotation.DependsOn;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.ApplicationProperties;
@Service @Service
@Slf4j
@DependsOn({"bookAndHtmlFormatsInstalled"}) @DependsOn({"bookAndHtmlFormatsInstalled"})
public class EndpointConfiguration { public class EndpointConfiguration {
private static final Logger logger = LoggerFactory.getLogger(EndpointConfiguration.class);
private Map<String, Boolean> endpointStatuses = new ConcurrentHashMap<>(); private Map<String, Boolean> endpointStatuses = new ConcurrentHashMap<>();
private Map<String, Set<String>> endpointGroups = new ConcurrentHashMap<>(); private Map<String, Set<String>> endpointGroups = new ConcurrentHashMap<>();
@@ -43,7 +43,7 @@ public class EndpointConfiguration {
public void disableEndpoint(String endpoint) { public void disableEndpoint(String endpoint) {
if (!endpointStatuses.containsKey(endpoint) || endpointStatuses.get(endpoint) != false) { if (!endpointStatuses.containsKey(endpoint) || endpointStatuses.get(endpoint) != false) {
logger.debug("Disabling {}", endpoint); log.debug("Disabling {}", endpoint);
endpointStatuses.put(endpoint, false); endpointStatuses.put(endpoint, false);
} }
} }
@@ -87,7 +87,7 @@ public class EndpointConfiguration {
.collect(Collectors.toList()); .collect(Collectors.toList());
if (!disabledList.isEmpty()) { if (!disabledList.isEmpty()) {
logger.info( log.info(
"Total disabled endpoints: {}. Disabled endpoints: {}", "Total disabled endpoints: {}. Disabled endpoints: {}",
disabledList.size(), disabledList.size(),
String.join(", ", disabledList)); String.join(", ", disabledList));
@@ -260,6 +260,9 @@ public class EndpointConfiguration {
// Pdftohtml dependent endpoints // Pdftohtml dependent endpoints
addEndpointToGroup("Pdftohtml", "pdf-to-html"); addEndpointToGroup("Pdftohtml", "pdf-to-html");
// disabled for now while we resolve issues
disableEndpoint("pdf-to-pdfa");
} }
private void processEnvironmentConfigs() { private void processEnvironmentConfigs() {

View File

@@ -1,11 +1,14 @@
package stirling.software.SPDF.config; package stirling.software.SPDF.config;
import java.io.IOException; import java.io.IOException;
import java.util.Properties;
import java.util.UUID; import java.util.UUID;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.Ordered; import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order; import org.springframework.core.annotation.Order;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import io.micrometer.common.util.StringUtils; import io.micrometer.common.util.StringUtils;
@@ -23,6 +26,18 @@ public class InitialSetup {
@Autowired private ApplicationProperties applicationProperties; @Autowired private ApplicationProperties applicationProperties;
@PostConstruct @PostConstruct
public void init() throws IOException {
initUUIDKey();
initSecretKey();
initEnableCSRFSecurity();
initLegalUrls();
initSetAppVersion();
}
public void initUUIDKey() throws IOException { public void initUUIDKey() throws IOException {
String uuid = applicationProperties.getAutomaticallyGenerated().getUUID(); String uuid = applicationProperties.getAutomaticallyGenerated().getUUID();
if (!GeneralUtils.isValidUUID(uuid)) { if (!GeneralUtils.isValidUUID(uuid)) {
@@ -32,7 +47,6 @@ public class InitialSetup {
} }
} }
@PostConstruct
public void initSecretKey() throws IOException { public void initSecretKey() throws IOException {
String secretKey = applicationProperties.getAutomaticallyGenerated().getKey(); String secretKey = applicationProperties.getAutomaticallyGenerated().getKey();
if (!GeneralUtils.isValidUUID(secretKey)) { if (!GeneralUtils.isValidUUID(secretKey)) {
@@ -42,13 +56,24 @@ public class InitialSetup {
} }
} }
@PostConstruct public void initEnableCSRFSecurity() throws IOException {
if (GeneralUtils.isVersionHigher(
"0.36.0", applicationProperties.getAutomaticallyGenerated().getAppVersion())) {
Boolean csrf = applicationProperties.getSecurity().getCsrfDisabled();
if (!csrf) {
GeneralUtils.saveKeyToConfig("security.csrfDisabled", false, false);
GeneralUtils.saveKeyToConfig("system.enableAnalytics", "true", false);
applicationProperties.getSecurity().setCsrfDisabled(false);
}
}
}
public void initLegalUrls() throws IOException { public void initLegalUrls() throws IOException {
// Initialize Terms and Conditions // Initialize Terms and Conditions
String termsUrl = applicationProperties.getLegal().getTermsAndConditions(); String termsUrl = applicationProperties.getLegal().getTermsAndConditions();
if (StringUtils.isEmpty(termsUrl)) { if (StringUtils.isEmpty(termsUrl)) {
String defaultTermsUrl = "https://www.stirlingpdf.com/terms-and-conditions"; String defaultTermsUrl = "https://www.stirlingpdf.com/terms-and-conditions";
GeneralUtils.saveKeyToConfig("legal.termsAndConditions", defaultTermsUrl); GeneralUtils.saveKeyToConfig("legal.termsAndConditions", defaultTermsUrl, false);
applicationProperties.getLegal().setTermsAndConditions(defaultTermsUrl); applicationProperties.getLegal().setTermsAndConditions(defaultTermsUrl);
} }
@@ -56,8 +81,23 @@ public class InitialSetup {
String privacyUrl = applicationProperties.getLegal().getPrivacyPolicy(); String privacyUrl = applicationProperties.getLegal().getPrivacyPolicy();
if (StringUtils.isEmpty(privacyUrl)) { if (StringUtils.isEmpty(privacyUrl)) {
String defaultPrivacyUrl = "https://www.stirlingpdf.com/privacy-policy"; String defaultPrivacyUrl = "https://www.stirlingpdf.com/privacy-policy";
GeneralUtils.saveKeyToConfig("legal.privacyPolicy", defaultPrivacyUrl); GeneralUtils.saveKeyToConfig("legal.privacyPolicy", defaultPrivacyUrl, false);
applicationProperties.getLegal().setPrivacyPolicy(defaultPrivacyUrl); applicationProperties.getLegal().setPrivacyPolicy(defaultPrivacyUrl);
} }
} }
public void initSetAppVersion() throws IOException {
String appVersion = "0.0.0";
Resource resource = new ClassPathResource("version.properties");
Properties props = new Properties();
try {
props.load(resource.getInputStream());
appVersion = props.getProperty("version");
} catch (Exception e) {
}
applicationProperties.getAutomaticallyGenerated().setAppVersion(appVersion);
GeneralUtils.saveKeyToConfig("AutomaticallyGenerated.appVersion", appVersion, false);
}
} }

View File

@@ -6,6 +6,7 @@ import java.util.List;
import stirling.software.SPDF.utils.FileInfo; import stirling.software.SPDF.utils.FileInfo;
public interface DatabaseBackupInterface { public interface DatabaseBackupInterface {
void exportDatabase() throws IOException; void exportDatabase() throws IOException;
boolean importDatabase(); boolean importDatabase();

View File

@@ -75,5 +75,6 @@ public class InitialSecuritySetup {
userService.addApiKeyToUser(Role.INTERNAL_API_USER.getRoleId()); userService.addApiKeyToUser(Role.INTERNAL_API_USER.getRoleId());
log.info("Internal API user created: " + Role.INTERNAL_API_USER.getRoleId()); log.info("Internal API user created: " + Role.INTERNAL_API_USER.getRoleId());
} }
userService.syncCustomApiUser(applicationProperties.getSecurity().getCustomGlobalAPIKey());
} }
} }

View File

@@ -1,6 +1,5 @@
package stirling.software.SPDF.config.security; package stirling.software.SPDF.config.security;
import java.io.IOException;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import java.util.*; import java.util.*;
@@ -13,7 +12,6 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn; import org.springframework.context.annotation.DependsOn;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.ProviderManager; import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
@@ -32,35 +30,21 @@ import org.springframework.security.oauth2.client.registration.InMemoryClientReg
import org.springframework.security.oauth2.core.user.OAuth2UserAuthority; import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
import org.springframework.security.saml2.core.Saml2X509Credential; import org.springframework.security.saml2.core.Saml2X509Credential;
import org.springframework.security.saml2.core.Saml2X509Credential.Saml2X509CredentialType; import org.springframework.security.saml2.core.Saml2X509Credential.Saml2X509CredentialType;
import org.springframework.security.saml2.provider.service.authentication.AbstractSaml2AuthenticationRequest;
import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider; import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider;
import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository; import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding; import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
import org.springframework.security.saml2.provider.service.web.HttpSessionSaml2AuthenticationRequestRepository;
import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationRequestRepository;
import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver; import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver;
import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository; import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy;
import org.springframework.security.web.context.SecurityContextHolderFilter;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository; import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler; import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
import org.springframework.security.web.savedrequest.NullRequestCache; import org.springframework.security.web.savedrequest.NullRequestCache;
import org.springframework.security.web.session.ForceEagerSessionCreationFilter;
import org.springframework.security.web.session.HttpSessionEventPublisher;
import org.springframework.security.web.session.SessionManagementFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.session.web.http.CookieSerializer;
import org.springframework.session.web.http.DefaultCookieSerializer;
import org.springframework.web.filter.OncePerRequestFilter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationFailureHandler; import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationFailureHandler;
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationSuccessHandler; import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationSuccessHandler;
@@ -115,7 +99,7 @@ public class SecurityConfiguration {
@Bean @Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
if (applicationProperties.getSecurity().getCsrfDisabled()) { if (applicationProperties.getSecurity().getCsrfDisabled() || !loginEnabledValue) {
http.csrf(csrf -> csrf.disable()); http.csrf(csrf -> csrf.disable());
} }
@@ -132,7 +116,7 @@ public class SecurityConfiguration {
csrf -> csrf ->
csrf.ignoringRequestMatchers( csrf.ignoringRequestMatchers(
request -> { request -> {
String apiKey = request.getHeader("X-API-Key"); String apiKey = request.getHeader("X-API-KEY");
// If there's no API key, don't ignore CSRF // If there's no API key, don't ignore CSRF
// (return false) // (return false)
@@ -163,7 +147,7 @@ public class SecurityConfiguration {
http.sessionManagement( http.sessionManagement(
sessionManagement -> sessionManagement ->
sessionManagement sessionManagement
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.maximumSessions(10) .maximumSessions(10)
.maxSessionsPreventsLogin(false) .maxSessionsPreventsLogin(false)
.sessionRegistry(sessionRegistry) .sessionRegistry(sessionRegistry)
@@ -287,7 +271,6 @@ public class SecurityConfiguration {
relyingPartyRegistrations()) relyingPartyRegistrations())
.authenticationManager( .authenticationManager(
new ProviderManager(authenticationProvider)) new ProviderManager(authenticationProvider))
.successHandler( .successHandler(
new CustomSaml2AuthenticationSuccessHandler( new CustomSaml2AuthenticationSuccessHandler(
loginAttemptService, loginAttemptService,
@@ -306,17 +289,17 @@ public class SecurityConfiguration {
} }
} else { } else {
if (!applicationProperties.getSecurity().getCsrfDisabled()) { // if (!applicationProperties.getSecurity().getCsrfDisabled()) {
CookieCsrfTokenRepository cookieRepo = // CookieCsrfTokenRepository cookieRepo =
CookieCsrfTokenRepository.withHttpOnlyFalse(); // CookieCsrfTokenRepository.withHttpOnlyFalse();
CsrfTokenRequestAttributeHandler requestHandler = // CsrfTokenRequestAttributeHandler requestHandler =
new CsrfTokenRequestAttributeHandler(); // new CsrfTokenRequestAttributeHandler();
requestHandler.setCsrfRequestAttributeName(null); // requestHandler.setCsrfRequestAttributeName(null);
http.csrf( // http.csrf(
csrf -> // csrf ->
csrf.csrfTokenRepository(cookieRepo) // csrf.csrfTokenRepository(cookieRepo)
.csrfTokenRequestHandler(requestHandler)); // .csrfTokenRequestHandler(requestHandler));
} // }
http.authorizeHttpRequests(authz -> authz.anyRequest().permitAll()); http.authorizeHttpRequests(authz -> authz.anyRequest().permitAll());
} }
@@ -452,7 +435,7 @@ public class SecurityConfiguration {
.clientName("OIDC") .clientName("OIDC")
.build()); .build());
} }
@Bean @Bean
@ConditionalOnProperty( @ConditionalOnProperty(
name = "security.saml2.enabled", name = "security.saml2.enabled",
@@ -506,7 +489,7 @@ public class SecurityConfiguration {
AuthnRequest authnRequest = customizer.getAuthnRequest(); AuthnRequest authnRequest = customizer.getAuthnRequest();
log.debug("AuthnRequest ID: {}", authnRequest.getID()); log.debug("AuthnRequest ID: {}", authnRequest.getID());
if (authnRequest.getID() == null) { if (authnRequest.getID() == null) {
authnRequest.setID("ARQ" + UUID.randomUUID().toString()); authnRequest.setID("ARQ" + UUID.randomUUID().toString());
} }

View File

@@ -71,7 +71,7 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
// Check for API key in the request headers if no authentication exists // Check for API key in the request headers if no authentication exists
if (authentication == null || !authentication.isAuthenticated()) { if (authentication == null || !authentication.isAuthenticated()) {
String apiKey = request.getHeader("X-API-Key"); String apiKey = request.getHeader("X-API-KEY");
if (apiKey != null && !apiKey.trim().isEmpty()) { if (apiKey != null && !apiKey.trim().isEmpty()) {
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

View File

@@ -59,7 +59,7 @@ public class UserBasedRateLimitingFilter extends OncePerRequestFilter {
String identifier = null; String identifier = null;
// Check for API key in the request headers // Check for API key in the request headers
String apiKey = request.getHeader("X-API-Key"); String apiKey = request.getHeader("X-API-KEY");
if (apiKey != null && !apiKey.trim().isEmpty()) { if (apiKey != null && !apiKey.trim().isEmpty()) {
identifier = identifier =
"API_KEY_" + apiKey; // Prefix to distinguish between API keys and usernames "API_KEY_" + apiKey; // Prefix to distinguish between API keys and usernames
@@ -79,7 +79,7 @@ public class UserBasedRateLimitingFilter extends OncePerRequestFilter {
Role userRole = Role userRole =
getRoleFromAuthentication(SecurityContextHolder.getContext().getAuthentication()); getRoleFromAuthentication(SecurityContextHolder.getContext().getAuthentication());
if (request.getHeader("X-API-Key") != null) { if (request.getHeader("X-API-KEY") != null) {
// It's an API call // It's an API call
processRequest( processRequest(
userRole.getApiCallsPerDay(), userRole.getApiCallsPerDay(),

View File

@@ -390,6 +390,37 @@ public class UserService implements UserServiceInterface {
} }
} }
@Transactional
public void syncCustomApiUser(String customApiKey) throws IOException {
if (customApiKey == null || customApiKey.trim().length() == 0) {
return;
}
String username = "CUSTOM_API_USER";
Optional<User> existingUser = findByUsernameIgnoreCase(username);
if (!existingUser.isPresent()) {
// Create new user with API role
User user = new User();
user.setUsername(username);
user.setPassword(UUID.randomUUID().toString());
user.setEnabled(true);
user.setFirstLogin(false);
user.setAuthenticationType(AuthenticationType.WEB);
user.setApiKey(customApiKey);
user.addAuthority(new Authority(Role.INTERNAL_API_USER.getRoleId(), user));
userRepository.save(user);
databaseBackupHelper.exportDatabase();
} else {
// Update API key if it has changed
User user = existingUser.get();
if (!customApiKey.equals(user.getApiKey())) {
user.setApiKey(customApiKey);
userRepository.save(user);
databaseBackupHelper.exportDatabase();
}
}
}
@Override @Override
public long getTotalUsersCount() { public long getTotalUsersCount() {
return userRepository.count(); return userRepository.count();

View File

@@ -2,8 +2,6 @@ package stirling.software.SPDF.config.security.oauth2;
import java.util.Optional; import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.LockedException; import org.springframework.security.authentication.LockedException;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService; import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
@@ -13,6 +11,7 @@ import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.security.LoginAttemptService; import stirling.software.SPDF.config.security.LoginAttemptService;
import stirling.software.SPDF.config.security.UserService; import stirling.software.SPDF.config.security.UserService;
import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.ApplicationProperties;
@@ -20,6 +19,7 @@ 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.User; import stirling.software.SPDF.model.User;
@Slf4j
public class CustomOAuth2UserService implements OAuth2UserService<OidcUserRequest, OidcUser> { public class CustomOAuth2UserService implements OAuth2UserService<OidcUserRequest, OidcUser> {
private final OidcUserService delegate = new OidcUserService(); private final OidcUserService delegate = new OidcUserService();
@@ -30,8 +30,6 @@ public class CustomOAuth2UserService implements OAuth2UserService<OidcUserReques
private ApplicationProperties applicationProperties; private ApplicationProperties applicationProperties;
private static final Logger logger = LoggerFactory.getLogger(CustomOAuth2UserService.class);
public CustomOAuth2UserService( public CustomOAuth2UserService(
ApplicationProperties applicationProperties, ApplicationProperties applicationProperties,
UserService userService, UserService userService,
@@ -82,10 +80,10 @@ public class CustomOAuth2UserService implements OAuth2UserService<OidcUserReques
user.getUserInfo(), user.getUserInfo(),
usernameAttribute); usernameAttribute);
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
logger.error("Error loading OIDC user: {}", e.getMessage()); log.error("Error loading OIDC user: {}", e.getMessage());
throw new OAuth2AuthenticationException(new OAuth2Error(e.getMessage()), e); throw new OAuth2AuthenticationException(new OAuth2Error(e.getMessage()), e);
} catch (Exception e) { } catch (Exception e) {
logger.error("Unexpected error loading OIDC user", e); log.error("Unexpected error loading OIDC user", e);
throw new OAuth2AuthenticationException("Unexpected error during authentication"); throw new OAuth2AuthenticationException("Unexpected error during authentication");
} }
} }

View File

@@ -11,8 +11,6 @@ import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.PDPageContentStream.AppendMode; 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.form.PDFormXObject; import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
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.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.ModelAttribute;
@@ -33,8 +31,6 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@Tag(name = "General", description = "General APIs") @Tag(name = "General", description = "General APIs")
public class CropController { public class CropController {
private static final Logger logger = LoggerFactory.getLogger(CropController.class);
private final CustomPDDocumentFactory pdfDocumentFactory; private final CustomPDDocumentFactory pdfDocumentFactory;
private final PostHogService postHogService; private final PostHogService postHogService;

View File

@@ -25,6 +25,7 @@ import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -34,24 +35,28 @@ import stirling.software.SPDF.config.security.database.DatabaseBackupHelper;
@Controller @Controller
@RequestMapping("/api/v1/database") @RequestMapping("/api/v1/database")
@PreAuthorize("hasRole('ROLE_ADMIN')") @PreAuthorize("hasRole('ROLE_ADMIN')")
@Tag(name = "Database", description = "Database APIs") @Tag(name = "Database", description = "Database APIs for backup, import, and management")
public class DatabaseController { public class DatabaseController {
@Autowired DatabaseBackupHelper databaseBackupHelper; @Autowired DatabaseBackupHelper databaseBackupHelper;
@Hidden
@PostMapping(consumes = "multipart/form-data", value = "import-database")
@Operation( @Operation(
summary = "Import database backup", summary = "Import a database backup file",
description = "This endpoint imports a database backup from a SQL file.") description = "Uploads and imports a database backup SQL file.")
@PostMapping(consumes = "multipart/form-data", value = "import-database")
public String importDatabase( public String importDatabase(
@RequestParam("fileInput") MultipartFile file, RedirectAttributes redirectAttributes) @Parameter(description = "SQL file to import", required = true)
throws IllegalArgumentException, IOException { @RequestParam("fileInput")
MultipartFile file,
RedirectAttributes redirectAttributes)
throws IOException {
if (file == null || file.isEmpty()) { if (file == null || file.isEmpty()) {
redirectAttributes.addAttribute("error", "fileNullOrEmpty"); redirectAttributes.addAttribute("error", "fileNullOrEmpty");
return "redirect:/database"; return "redirect:/database";
} }
log.info("Received file: {}", file.getOriginalFilename()); log.info("Received file: {}", file.getOriginalFilename());
Path tempTemplatePath = Files.createTempFile("backup_", ".sql"); Path tempTemplatePath = Files.createTempFile("backup_", ".sql");
try (InputStream in = file.getInputStream()) { try (InputStream in = file.getInputStream()) {
Files.copy(in, tempTemplatePath, StandardCopyOption.REPLACE_EXISTING); Files.copy(in, tempTemplatePath, StandardCopyOption.REPLACE_EXISTING);
@@ -69,9 +74,15 @@ public class DatabaseController {
} }
@Hidden @Hidden
@Operation(
summary = "Import database backup by filename",
description = "Imports a database backup file from the server using its file name.")
@GetMapping("/import-database-file/{fileName}") @GetMapping("/import-database-file/{fileName}")
public String importDatabaseFromBackupUI(@PathVariable String fileName) public String importDatabaseFromBackupUI(
throws IllegalArgumentException, IOException { @Parameter(description = "Name of the file to import", required = true) @PathVariable
String fileName)
throws IOException {
if (fileName == null || fileName.isEmpty()) { if (fileName == null || fileName.isEmpty()) {
return "redirect:/database?error=fileNullOrEmpty"; return "redirect:/database?error=fileNullOrEmpty";
} }
@@ -85,6 +96,7 @@ public class DatabaseController {
return "redirect:/database?error=fileNotFound"; return "redirect:/database?error=fileNotFound";
} }
log.info("Received file: {}", fileName); log.info("Received file: {}", fileName);
if (databaseBackupHelper.importDatabaseFromUI(fileName)) { if (databaseBackupHelper.importDatabaseFromUI(fileName)) {
log.info("File {} imported to database", fileName); log.info("File {} imported to database", fileName);
return "redirect:/database?infoMessage=importIntoDatabaseSuccessed"; return "redirect:/database?infoMessage=importIntoDatabaseSuccessed";
@@ -93,12 +105,14 @@ public class DatabaseController {
} }
@Hidden @Hidden
@GetMapping("/delete/{fileName}")
@Operation( @Operation(
summary = "Delete a database backup file", summary = "Delete a database backup file",
description = description = "Deletes a specified database backup file from the server.")
"This endpoint deletes a database backup file with the specified file name.") @GetMapping("/delete/{fileName}")
public String deleteFile(@PathVariable String fileName) { public String deleteFile(
@Parameter(description = "Name of the file to delete", required = true) @PathVariable
String fileName) {
if (fileName == null || fileName.isEmpty()) { if (fileName == null || fileName.isEmpty()) {
throw new IllegalArgumentException("File must not be null or empty"); throw new IllegalArgumentException("File must not be null or empty");
} }
@@ -117,12 +131,13 @@ public class DatabaseController {
} }
@Hidden @Hidden
@GetMapping("/download/{fileName}")
@Operation( @Operation(
summary = "Download a database backup file", summary = "Download a database backup file",
description = description = "Downloads the specified database backup file from the server.")
"This endpoint downloads a database backup file with the specified file name.") @GetMapping("/download/{fileName}")
public ResponseEntity<?> downloadFile(@PathVariable String fileName) { public ResponseEntity<?> downloadFile(
@Parameter(description = "Name of the file to download", required = true) @PathVariable
String fileName) {
if (fileName == null || fileName.isEmpty()) { if (fileName == null || fileName.isEmpty()) {
throw new IllegalArgumentException("File must not be null or empty"); throw new IllegalArgumentException("File must not be null or empty");
} }
@@ -141,4 +156,22 @@ public class DatabaseController {
.build(); .build();
} }
} }
@Operation(
summary = "Create a database backup",
description =
"This endpoint triggers the creation of a database backup and redirects to the"
+ " database management page.")
@GetMapping("/createDatabaseBackup")
public String createDatabaseBackup() {
try {
log.info("Starting database backup creation...");
databaseBackupHelper.exportDatabase();
log.info("Database backup successfully created.");
} catch (IOException e) {
log.error("Error creating database backup: {}", e.getMessage(), e);
return "redirect:/database?error=" + e.getMessage();
}
return "redirect:/database?infoMessage=backupCreated";
}
} }

View File

@@ -20,8 +20,6 @@ import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm; import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
import org.apache.pdfbox.pdmodel.interactive.form.PDField; import org.apache.pdfbox.pdmodel.interactive.form.PDField;
import org.apache.pdfbox.pdmodel.interactive.form.PDSignatureField; import org.apache.pdfbox.pdmodel.interactive.form.PDSignatureField;
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.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.ModelAttribute;
@@ -33,18 +31,18 @@ import org.springframework.web.multipart.MultipartFile;
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 lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.api.general.MergePdfsRequest; import stirling.software.SPDF.model.api.general.MergePdfsRequest;
import stirling.software.SPDF.service.CustomPDDocumentFactory; import stirling.software.SPDF.service.CustomPDDocumentFactory;
import stirling.software.SPDF.utils.GeneralUtils; import stirling.software.SPDF.utils.GeneralUtils;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@Slf4j
@RequestMapping("/api/v1/general") @RequestMapping("/api/v1/general")
@Tag(name = "General", description = "General APIs") @Tag(name = "General", description = "General APIs")
public class MergeController { public class MergeController {
private static final Logger logger = LoggerFactory.getLogger(MergeController.class);
private final CustomPDDocumentFactory pdfDocumentFactory; private final CustomPDDocumentFactory pdfDocumentFactory;
@Autowired @Autowired
@@ -184,7 +182,7 @@ public class MergeController {
baos.toByteArray(), mergedFileName); // Return the modified PDF baos.toByteArray(), mergedFileName); // Return the modified PDF
} catch (Exception ex) { } catch (Exception ex) {
logger.error("Error in merge pdf process", ex); log.error("Error in merge pdf process", ex);
throw ex; throw ex;
} finally { } finally {
for (File file : filesToDelete) { for (File file : filesToDelete) {

View File

@@ -12,8 +12,6 @@ import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.common.PDRectangle; import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject; import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
import org.apache.pdfbox.util.Matrix; import org.apache.pdfbox.util.Matrix;
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.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.ModelAttribute;
@@ -35,8 +33,6 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@Tag(name = "General", description = "General APIs") @Tag(name = "General", description = "General APIs")
public class MultiPageLayoutController { public class MultiPageLayoutController {
private static final Logger logger = LoggerFactory.getLogger(MultiPageLayoutController.class);
private final CustomPDDocumentFactory pdfDocumentFactory; private final CustomPDDocumentFactory pdfDocumentFactory;
@Autowired @Autowired

View File

@@ -8,8 +8,6 @@ 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.PDPage;
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.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.ModelAttribute;
@@ -22,6 +20,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 lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.SortTypes; import stirling.software.SPDF.model.SortTypes;
import stirling.software.SPDF.model.api.PDFWithPageNums; import stirling.software.SPDF.model.api.PDFWithPageNums;
import stirling.software.SPDF.model.api.general.RearrangePagesRequest; import stirling.software.SPDF.model.api.general.RearrangePagesRequest;
@@ -31,11 +30,10 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@RequestMapping("/api/v1/general") @RequestMapping("/api/v1/general")
@Slf4j
@Tag(name = "General", description = "General APIs") @Tag(name = "General", description = "General APIs")
public class RearrangePagesPDFController { public class RearrangePagesPDFController {
private static final Logger logger = LoggerFactory.getLogger(RearrangePagesPDFController.class);
private final CustomPDDocumentFactory pdfDocumentFactory; private final CustomPDDocumentFactory pdfDocumentFactory;
@Autowired @Autowired
@@ -202,7 +200,7 @@ public class RearrangePagesPDFController {
throw new IllegalArgumentException("Unsupported custom mode"); throw new IllegalArgumentException("Unsupported custom mode");
} }
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
logger.error("Unsupported custom mode", e); log.error("Unsupported custom mode", e);
return null; return null;
} }
} }
@@ -230,8 +228,8 @@ public class RearrangePagesPDFController {
} else { } else {
newPageOrder = GeneralUtils.parsePageList(pageOrderArr, totalPages, false); newPageOrder = GeneralUtils.parsePageList(pageOrderArr, totalPages, false);
} }
logger.info("newPageOrder = " + newPageOrder); log.info("newPageOrder = " + newPageOrder);
logger.info("totalPages = " + totalPages); log.info("totalPages = " + totalPages);
// Create a new list to hold the pages in the new order // Create a new list to hold the pages in the new order
List<PDPage> newPages = new ArrayList<>(); List<PDPage> newPages = new ArrayList<>();
for (int i = 0; i < newPageOrder.size(); i++) { for (int i = 0; i < newPageOrder.size(); i++) {
@@ -254,7 +252,7 @@ public class RearrangePagesPDFController {
.replaceFirst("[.][^.]+$", "") .replaceFirst("[.][^.]+$", "")
+ "_rearranged.pdf"); + "_rearranged.pdf");
} catch (IOException e) { } catch (IOException e) {
logger.error("Failed rearranging documents", e); log.error("Failed rearranging documents", e);
return null; return null;
} }
} }

View File

@@ -5,8 +5,6 @@ import java.io.IOException;
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageTree; import org.apache.pdfbox.pdmodel.PDPageTree;
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.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.ModelAttribute;
@@ -28,8 +26,6 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@Tag(name = "General", description = "General APIs") @Tag(name = "General", description = "General APIs")
public class RotationController { public class RotationController {
private static final Logger logger = LoggerFactory.getLogger(RotationController.class);
private final CustomPDDocumentFactory pdfDocumentFactory; private final CustomPDDocumentFactory pdfDocumentFactory;
@Autowired @Autowired

View File

@@ -13,8 +13,6 @@ import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.common.PDRectangle; import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject; import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
import org.apache.pdfbox.util.Matrix; import org.apache.pdfbox.util.Matrix;
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.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.ModelAttribute;
@@ -36,8 +34,6 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@Tag(name = "General", description = "General APIs") @Tag(name = "General", description = "General APIs")
public class ScalePagesController { public class ScalePagesController {
private static final Logger logger = LoggerFactory.getLogger(ScalePagesController.class);
private final CustomPDDocumentFactory pdfDocumentFactory; private final CustomPDDocumentFactory pdfDocumentFactory;
@Autowired @Autowired

View File

@@ -13,8 +13,6 @@ 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;
import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPage;
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.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
@@ -28,16 +26,17 @@ 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 lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.api.PDFWithPageNums; import stirling.software.SPDF.model.api.PDFWithPageNums;
import stirling.software.SPDF.service.CustomPDDocumentFactory; import stirling.software.SPDF.service.CustomPDDocumentFactory;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@RequestMapping("/api/v1/general") @RequestMapping("/api/v1/general")
@Slf4j
@Tag(name = "General", description = "General APIs") @Tag(name = "General", description = "General APIs")
public class SplitPDFController { public class SplitPDFController {
private static final Logger logger = LoggerFactory.getLogger(SplitPDFController.class);
private final CustomPDDocumentFactory pdfDocumentFactory; private final CustomPDDocumentFactory pdfDocumentFactory;
@Autowired @Autowired
@@ -52,84 +51,114 @@ public class SplitPDFController {
"This endpoint splits a given PDF file into separate documents based on the specified page numbers or ranges. Users can specify pages using individual numbers, ranges, or 'all' for every page. Input:PDF Output:PDF Type:SIMO") "This endpoint splits a given PDF file into separate documents based on the specified page numbers or ranges. Users can specify pages using individual numbers, ranges, or 'all' for every page. Input:PDF Output:PDF Type:SIMO")
public ResponseEntity<byte[]> splitPdf(@ModelAttribute PDFWithPageNums request) public ResponseEntity<byte[]> splitPdf(@ModelAttribute PDFWithPageNums request)
throws IOException { throws IOException {
MultipartFile file = request.getFileInput();
String pages = request.getPageNumbers();
// open the pdf document
PDDocument document = Loader.loadPDF(file.getBytes()); PDDocument document = null;
// PdfMetadata metadata = PdfMetadataService.extractMetadataFromPdf(document); Path zipFile = null;
int totalPages = document.getNumberOfPages();
List<Integer> pageNumbers = request.getPageNumbersList(document, false);
if (!pageNumbers.contains(totalPages - 1)) {
// Create a mutable ArrayList so we can add to it
pageNumbers = new ArrayList<>(pageNumbers);
pageNumbers.add(totalPages - 1);
}
logger.info(
"Splitting PDF into pages: {}",
pageNumbers.stream().map(String::valueOf).collect(Collectors.joining(",")));
// split the document
List<ByteArrayOutputStream> splitDocumentsBoas = new ArrayList<>(); List<ByteArrayOutputStream> splitDocumentsBoas = new ArrayList<>();
int previousPageNumber = 0;
for (int splitPoint : pageNumbers) { try {
try (PDDocument splitDocument =
pdfDocumentFactory.createNewDocumentBasedOnOldDocument(document)) { MultipartFile file = request.getFileInput();
for (int i = previousPageNumber; i <= splitPoint; i++) { String pages = request.getPageNumbers();
PDPage page = document.getPage(i); // open the pdf document
splitDocument.addPage(page);
logger.info("Adding page {} to split document", i); document = Loader.loadPDF(file.getBytes());
// PdfMetadata metadata = PdfMetadataService.extractMetadataFromPdf(document);
int totalPages = document.getNumberOfPages();
List<Integer> pageNumbers = request.getPageNumbersList(document, false);
if (!pageNumbers.contains(totalPages - 1)) {
// Create a mutable ArrayList so we can add to it
pageNumbers = new ArrayList<>(pageNumbers);
pageNumbers.add(totalPages - 1);
}
log.info(
"Splitting PDF into pages: {}",
pageNumbers.stream().map(String::valueOf).collect(Collectors.joining(",")));
// split the document
splitDocumentsBoas = new ArrayList<>();
int previousPageNumber = 0;
for (int splitPoint : pageNumbers) {
try (PDDocument splitDocument =
pdfDocumentFactory.createNewDocumentBasedOnOldDocument(document)) {
for (int i = previousPageNumber; i <= splitPoint; i++) {
PDPage page = document.getPage(i);
splitDocument.addPage(page);
log.info("Adding page {} to split document", i);
}
previousPageNumber = splitPoint + 1;
// Transfer metadata to split pdf
// PdfMetadataService.setMetadataToPdf(splitDocument, metadata);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
splitDocument.save(baos);
splitDocumentsBoas.add(baos);
} catch (Exception e) {
log.error("Failed splitting documents and saving them", e);
throw e;
} }
previousPageNumber = splitPoint + 1; }
// Transfer metadata to split pdf // closing the original document
// PdfMetadataService.setMetadataToPdf(splitDocument, metadata); document.close();
ByteArrayOutputStream baos = new ByteArrayOutputStream(); zipFile = Files.createTempFile("split_documents", ".zip");
splitDocument.save(baos);
splitDocumentsBoas.add(baos); String filename =
Filenames.toSimpleFileName(file.getOriginalFilename())
.replaceFirst("[.][^.]+$", "");
try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(zipFile))) {
// loop through the split documents and write them to the zip file
for (int i = 0; i < splitDocumentsBoas.size(); i++) {
String fileName = filename + "_" + (i + 1) + ".pdf";
ByteArrayOutputStream baos = splitDocumentsBoas.get(i);
byte[] pdf = baos.toByteArray();
// Add PDF file to the zip
ZipEntry pdfEntry = new ZipEntry(fileName);
zipOut.putNextEntry(pdfEntry);
zipOut.write(pdf);
zipOut.closeEntry();
log.info("Wrote split document {} to zip file", fileName);
}
} catch (Exception e) { } catch (Exception e) {
logger.error("Failed splitting documents and saving them", e); log.error("Failed writing to zip", e);
throw e; throw e;
} }
}
// closing the original document log.info("Successfully created zip file with split documents: {}", zipFile.toString());
document.close(); byte[] data = Files.readAllBytes(zipFile);
Files.deleteIfExists(zipFile);
Path zipFile = Files.createTempFile("split_documents", ".zip"); // return the Resource in the response
return WebResponseUtils.bytesToWebResponse(
data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM);
String filename = } finally {
Filenames.toSimpleFileName(file.getOriginalFilename()) try {
.replaceFirst("[.][^.]+$", ""); // Close the main document
try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(zipFile))) { if (document != null) {
// loop through the split documents and write them to the zip file document.close();
for (int i = 0; i < splitDocumentsBoas.size(); i++) { }
String fileName = filename + "_" + (i + 1) + ".pdf";
ByteArrayOutputStream baos = splitDocumentsBoas.get(i);
byte[] pdf = baos.toByteArray();
// Add PDF file to the zip // Close all ByteArrayOutputStreams
ZipEntry pdfEntry = new ZipEntry(fileName); for (ByteArrayOutputStream baos : splitDocumentsBoas) {
zipOut.putNextEntry(pdfEntry); if (baos != null) {
zipOut.write(pdf); baos.close();
zipOut.closeEntry(); }
}
logger.info("Wrote split document {} to zip file", fileName); // Delete temporary zip file
if (zipFile != null) {
Files.deleteIfExists(zipFile);
}
} catch (Exception e) {
log.error("Error while cleaning up resources", e);
} }
} catch (Exception e) {
logger.error("Failed writing to zip", e);
throw e;
} }
logger.info("Successfully created zip file with split documents: {}", zipFile.toString());
byte[] data = Files.readAllBytes(zipFile);
Files.deleteIfExists(zipFile);
// return the Resource in the response
return WebResponseUtils.bytesToWebResponse(
data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM);
} }
} }

View File

@@ -13,8 +13,6 @@ import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDDocumentOutline; import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDDocumentOutline;
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDOutlineItem; import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDOutlineItem;
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.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
@@ -32,6 +30,7 @@ import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.PdfMetadata; import stirling.software.SPDF.model.PdfMetadata;
import stirling.software.SPDF.model.api.SplitPdfByChaptersRequest; import stirling.software.SPDF.model.api.SplitPdfByChaptersRequest;
import stirling.software.SPDF.service.PdfMetadataService; import stirling.software.SPDF.service.PdfMetadataService;
@@ -39,12 +38,10 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@RequestMapping("/api/v1/general") @RequestMapping("/api/v1/general")
@Slf4j
@Tag(name = "General", description = "General APIs") @Tag(name = "General", description = "General APIs")
public class SplitPdfByChaptersController { public class SplitPdfByChaptersController {
private static final Logger logger =
LoggerFactory.getLogger(SplitPdfByChaptersController.class);
private final PdfMetadataService pdfMetadataService; private final PdfMetadataService pdfMetadataService;
@Autowired @Autowired
@@ -59,70 +56,86 @@ public class SplitPdfByChaptersController {
public ResponseEntity<byte[]> splitPdf(@ModelAttribute SplitPdfByChaptersRequest request) public ResponseEntity<byte[]> splitPdf(@ModelAttribute SplitPdfByChaptersRequest request)
throws Exception { throws Exception {
MultipartFile file = request.getFileInput(); MultipartFile file = request.getFileInput();
boolean includeMetadata = request.getIncludeMetadata(); PDDocument sourceDocument = null;
Integer bookmarkLevel = Path zipFile = null;
request.getBookmarkLevel(); // levels start from 0 (top most bookmarks)
if (bookmarkLevel < 0) {
return ResponseEntity.badRequest().body("Invalid bookmark level".getBytes());
}
PDDocument sourceDocument = Loader.loadPDF(file.getBytes());
PDDocumentOutline outline = sourceDocument.getDocumentCatalog().getDocumentOutline();
if (outline == null) {
logger.warn("No outline found for {}", file.getOriginalFilename());
return ResponseEntity.badRequest().body("No outline found".getBytes());
}
List<Bookmark> bookmarks = new ArrayList<>();
try { try {
bookmarks = boolean includeMetadata = request.getIncludeMetadata();
extractOutlineItems( Integer bookmarkLevel =
sourceDocument, request.getBookmarkLevel(); // levels start from 0 (top most bookmarks)
outline.getFirstChild(), if (bookmarkLevel < 0) {
bookmarks, return ResponseEntity.badRequest().body("Invalid bookmark level".getBytes());
outline.getFirstChild().getNextSibling(), }
0, sourceDocument = Loader.loadPDF(file.getBytes());
bookmarkLevel);
// to handle last page edge case
bookmarks.get(bookmarks.size() - 1).setEndPage(sourceDocument.getNumberOfPages());
Bookmark lastBookmark = bookmarks.get(bookmarks.size() - 1);
} catch (Exception e) { PDDocumentOutline outline = sourceDocument.getDocumentCatalog().getDocumentOutline();
logger.error("Unable to extract outline items", e);
return ResponseEntity.internalServerError() if (outline == null) {
.body("Unable to extract outline items".getBytes()); log.warn("No outline found for {}", file.getOriginalFilename());
return ResponseEntity.badRequest().body("No outline found".getBytes());
}
List<Bookmark> bookmarks = new ArrayList<>();
try {
bookmarks =
extractOutlineItems(
sourceDocument,
outline.getFirstChild(),
bookmarks,
outline.getFirstChild().getNextSibling(),
0,
bookmarkLevel);
// to handle last page edge case
bookmarks.get(bookmarks.size() - 1).setEndPage(sourceDocument.getNumberOfPages());
Bookmark lastBookmark = bookmarks.get(bookmarks.size() - 1);
} catch (Exception e) {
log.error("Unable to extract outline items", e);
return ResponseEntity.internalServerError()
.body("Unable to extract outline items".getBytes());
}
boolean allowDuplicates = request.getAllowDuplicates();
if (!allowDuplicates) {
/*
duplicates are generated when multiple bookmarks correspond to the same page,
if the user doesn't want duplicates mergeBookmarksThatCorrespondToSamePage() method will merge the titles of all
the bookmarks that correspond to the same page, and treat them as a single bookmark
*/
bookmarks = mergeBookmarksThatCorrespondToSamePage(bookmarks);
}
for (Bookmark bookmark : bookmarks) {
log.info(
"{}::::{} to {}",
bookmark.getTitle(),
bookmark.getStartPage(),
bookmark.getEndPage());
}
List<ByteArrayOutputStream> splitDocumentsBoas =
getSplitDocumentsBoas(sourceDocument, bookmarks, includeMetadata);
zipFile = createZipFile(bookmarks, splitDocumentsBoas);
byte[] data = Files.readAllBytes(zipFile);
Files.deleteIfExists(zipFile);
String filename =
Filenames.toSimpleFileName(file.getOriginalFilename())
.replaceFirst("[.][^.]+$", "");
sourceDocument.close();
return WebResponseUtils.bytesToWebResponse(
data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM);
} finally {
try {
if (sourceDocument != null) {
sourceDocument.close();
}
if (zipFile != null) {
Files.deleteIfExists(zipFile);
}
} catch (Exception e) {
log.error("Error while cleaning up resources", e);
}
} }
boolean allowDuplicates = request.getAllowDuplicates();
if (!allowDuplicates) {
/*
duplicates are generated when multiple bookmarks correspond to the same page,
if the user doesn't want duplicates mergeBookmarksThatCorrespondToSamePage() method will merge the titles of all
the bookmarks that correspond to the same page, and treat them as a single bookmark
*/
bookmarks = mergeBookmarksThatCorrespondToSamePage(bookmarks);
}
for (Bookmark bookmark : bookmarks) {
logger.info(
"{}::::{} to {}",
bookmark.getTitle(),
bookmark.getStartPage(),
bookmark.getEndPage());
}
List<ByteArrayOutputStream> splitDocumentsBoas =
getSplitDocumentsBoas(sourceDocument, bookmarks, includeMetadata);
Path zipFile = createZipFile(bookmarks, splitDocumentsBoas);
byte[] data = Files.readAllBytes(zipFile);
Files.deleteIfExists(zipFile);
String filename =
Filenames.toSimpleFileName(file.getOriginalFilename())
.replaceFirst("[.][^.]+$", "");
sourceDocument.close();
return WebResponseUtils.bytesToWebResponse(
data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM);
} }
private List<Bookmark> mergeBookmarksThatCorrespondToSamePage(List<Bookmark> bookmarks) { private List<Bookmark> mergeBookmarksThatCorrespondToSamePage(List<Bookmark> bookmarks) {
@@ -240,14 +253,14 @@ public class SplitPdfByChaptersController {
zipOut.write(pdf); zipOut.write(pdf);
zipOut.closeEntry(); zipOut.closeEntry();
logger.info("Wrote split document {} to zip file", fileName); log.info("Wrote split document {} to zip file", fileName);
} }
} catch (Exception e) { } catch (Exception e) {
logger.error("Failed writing to zip", e); log.error("Failed writing to zip", e);
throw e; throw e;
} }
logger.info("Successfully created zip file with split documents: {}", zipFile); log.info("Successfully created zip file with split documents: {}", zipFile);
return zipFile; return zipFile;
} }
@@ -268,7 +281,7 @@ public class SplitPdfByChaptersController {
i++) { i++) {
PDPage page = sourceDocument.getPage(i); PDPage page = sourceDocument.getPage(i);
splitDocument.addPage(page); splitDocument.addPage(page);
logger.info("Adding page {} to split document", i); log.info("Adding page {} to split document", i);
} }
ByteArrayOutputStream baos = new ByteArrayOutputStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream();
if (includeMetadata) { if (includeMetadata) {
@@ -279,7 +292,7 @@ public class SplitPdfByChaptersController {
splitDocumentsBoas.add(baos); splitDocumentsBoas.add(baos);
} catch (Exception e) { } catch (Exception e) {
logger.error("Failed splitting documents and saving them", e); log.error("Failed splitting documents and saving them", e);
throw e; throw e;
} }
} }

View File

@@ -18,8 +18,6 @@ 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.form.PDFormXObject; import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
import org.apache.pdfbox.util.Matrix; import org.apache.pdfbox.util.Matrix;
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.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
@@ -42,9 +40,6 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@Tag(name = "General", description = "General APIs") @Tag(name = "General", description = "General APIs")
public class SplitPdfBySectionsController { public class SplitPdfBySectionsController {
private static final Logger logger =
LoggerFactory.getLogger(SplitPdfBySectionsController.class);
private final CustomPDDocumentFactory pdfDocumentFactory; private final CustomPDDocumentFactory pdfDocumentFactory;
@Autowired @Autowired
@@ -105,15 +100,13 @@ public class SplitPdfBySectionsController {
if (sectionNum == horiz * verti) pageNum++; if (sectionNum == horiz * verti) pageNum++;
} }
} catch (Exception e) {
logger.error("exception", e);
} finally {
data = Files.readAllBytes(zipFile); data = Files.readAllBytes(zipFile);
return WebResponseUtils.bytesToWebResponse(
data, filename + "_split.zip", MediaType.APPLICATION_OCTET_STREAM);
} finally {
Files.deleteIfExists(zipFile); Files.deleteIfExists(zipFile);
} }
return WebResponseUtils.bytesToWebResponse(
data, filename + "_split.zip", MediaType.APPLICATION_OCTET_STREAM);
} }
public List<PDDocument> splitPdfPages( public List<PDDocument> splitPdfPages(

View File

@@ -10,8 +10,6 @@ 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;
import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPage;
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.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
@@ -25,6 +23,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 lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.api.general.SplitPdfBySizeOrCountRequest; import stirling.software.SPDF.model.api.general.SplitPdfBySizeOrCountRequest;
import stirling.software.SPDF.service.CustomPDDocumentFactory; import stirling.software.SPDF.service.CustomPDDocumentFactory;
import stirling.software.SPDF.utils.GeneralUtils; import stirling.software.SPDF.utils.GeneralUtils;
@@ -32,10 +31,10 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@RequestMapping("/api/v1/general") @RequestMapping("/api/v1/general")
@Slf4j
@Tag(name = "General", description = "General APIs") @Tag(name = "General", description = "General APIs")
public class SplitPdfBySizeController { public class SplitPdfBySizeController {
private static final Logger logger = LoggerFactory.getLogger(SplitPdfBySizeController.class);
private final CustomPDDocumentFactory pdfDocumentFactory; private final CustomPDDocumentFactory pdfDocumentFactory;
@Autowired @Autowired
@@ -78,7 +77,7 @@ public class SplitPdfBySizeController {
} }
} catch (Exception e) { } catch (Exception e) {
logger.error("exception", e); log.error("exception", e);
} finally { } finally {
data = Files.readAllBytes(zipFile); data = Files.readAllBytes(zipFile);
Files.deleteIfExists(zipFile); Files.deleteIfExists(zipFile);

View File

@@ -11,8 +11,6 @@ import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream; import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.common.PDRectangle; import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject; import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
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.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.ModelAttribute;
@@ -32,8 +30,6 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@Tag(name = "General", description = "General APIs") @Tag(name = "General", description = "General APIs")
public class ToSinglePageController { public class ToSinglePageController {
private static final Logger logger = LoggerFactory.getLogger(ToSinglePageController.class);
private final CustomPDDocumentFactory pdfDocumentFactory; private final CustomPDDocumentFactory pdfDocumentFactory;
@Autowired @Autowired

View File

@@ -14,8 +14,6 @@ import java.util.zip.ZipOutputStream;
import org.apache.commons.io.FileUtils; 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.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
@@ -29,6 +27,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 lombok.extern.slf4j.Slf4j;
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.service.CustomPDDocumentFactory; import stirling.software.SPDF.service.CustomPDDocumentFactory;
@@ -40,11 +39,10 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@RequestMapping("/api/v1/convert") @RequestMapping("/api/v1/convert")
@Slf4j
@Tag(name = "Convert", description = "Convert APIs") @Tag(name = "Convert", description = "Convert APIs")
public class ConvertImgPDFController { public class ConvertImgPDFController {
private static final Logger logger = LoggerFactory.getLogger(ConvertImgPDFController.class);
private final CustomPDDocumentFactory pdfDocumentFactory; private final CustomPDDocumentFactory pdfDocumentFactory;
@Autowired @Autowired
@@ -65,112 +63,137 @@ public class ConvertImgPDFController {
String colorType = request.getColorType(); String colorType = request.getColorType();
String dpi = request.getDpi(); String dpi = request.getDpi();
byte[] pdfBytes = file.getBytes(); Path tempFile = null;
ImageType colorTypeResult = ImageType.RGB; Path tempOutputDir = null;
if ("greyscale".equals(colorType)) { Path tempPdfPath = null;
colorTypeResult = ImageType.GRAY;
} else if ("blackwhite".equals(colorType)) {
colorTypeResult = ImageType.BINARY;
}
// returns bytes for image
boolean singleImage = "single".equals(singleOrMultiple);
byte[] result = null; byte[] result = null;
String filename =
Filenames.toSimpleFileName(file.getOriginalFilename())
.replaceFirst("[.][^.]+$", "");
result = try {
PdfUtils.convertFromPdf( byte[] pdfBytes = file.getBytes();
pdfBytes, ImageType colorTypeResult = ImageType.RGB;
"webp".equalsIgnoreCase(imageFormat) ? "png" : imageFormat.toUpperCase(), if ("greyscale".equals(colorType)) {
colorTypeResult, colorTypeResult = ImageType.GRAY;
singleImage, } else if ("blackwhite".equals(colorType)) {
Integer.valueOf(dpi), colorTypeResult = ImageType.BINARY;
filename);
if (result == null || result.length == 0) {
logger.error("resultant bytes for {} is null, error converting ", filename);
}
if ("webp".equalsIgnoreCase(imageFormat) && !CheckProgramInstall.isPythonAvailable()) {
throw new IOException("Python is not installed. Required for WebP conversion.");
} else if ("webp".equalsIgnoreCase(imageFormat)
&& CheckProgramInstall.isPythonAvailable()) {
// 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();
} }
// returns bytes for image
boolean singleImage = "single".equals(singleOrMultiple);
String filename =
Filenames.toSimpleFileName(file.getOriginalFilename())
.replaceFirst("[.][^.]+$", "");
String pythonVersion = CheckProgramInstall.getAvailablePythonCommand(); result =
PdfUtils.convertFromPdf(
List<String> command = new ArrayList<>(); pdfBytes,
command.add(pythonVersion); "webp".equalsIgnoreCase(imageFormat)
command.add("./scripts/png_to_webp.py"); // Python script to handle the conversion ? "png"
: imageFormat.toUpperCase(),
// Create a temporary directory for the output WebP files colorTypeResult,
Path tempOutputDir = Files.createTempDirectory("webp_output"); singleImage,
if (singleImage) { Integer.valueOf(dpi),
// Run the Python script to convert PNG to WebP filename);
command.add(tempFile.toString()); if (result == null || result.length == 0) {
command.add(tempOutputDir.toString()); log.error("resultant bytes for {} is null, error converting ", filename);
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"); if ("webp".equalsIgnoreCase(imageFormat) && !CheckProgramInstall.isPythonAvailable()) {
command.add(dpi); throw new IOException("Python is not installed. Required for WebP conversion.");
ProcessExecutorResult resultProcess = } else if ("webp".equalsIgnoreCase(imageFormat)
ProcessExecutor.getInstance(ProcessExecutor.Processes.PYTHON_OPENCV) && CheckProgramInstall.isPythonAvailable()) {
.runCommandWithOutputHandling(command); // Write the output stream to a temp file
tempFile = Files.createTempFile("temp_png", ".png");
// Find all WebP files in the output directory try (FileOutputStream fos = new FileOutputStream(tempFile.toFile())) {
List<Path> webpFiles = fos.write(result);
Files.walk(tempOutputDir) fos.flush();
.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) { String pythonVersion = CheckProgramInstall.getAvailablePythonCommand();
String docName = filename + "." + imageFormat;
MediaType mediaType = MediaType.parseMediaType(getMediaType(imageFormat)); List<String> command = new ArrayList<>();
return WebResponseUtils.bytesToWebResponse(result, docName, mediaType); command.add(pythonVersion);
} else { command.add("./scripts/png_to_webp.py"); // Python script to handle the conversion
String zipFilename = filename + "_convertedToImages.zip";
return WebResponseUtils.bytesToWebResponse( // Create a temporary directory for the output WebP files
result, zipFilename, MediaType.APPLICATION_OCTET_STREAM); 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
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()) {
log.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) {
String docName = filename + "." + imageFormat;
MediaType mediaType = MediaType.parseMediaType(getMediaType(imageFormat));
return WebResponseUtils.bytesToWebResponse(result, docName, mediaType);
} else {
String zipFilename = filename + "_convertedToImages.zip";
return WebResponseUtils.bytesToWebResponse(
result, zipFilename, MediaType.APPLICATION_OCTET_STREAM);
}
} finally {
try {
// Clean up temporary files
if (tempFile != null) {
Files.deleteIfExists(tempFile);
}
if (tempPdfPath != null) {
Files.deleteIfExists(tempPdfPath);
}
if (tempOutputDir != null) {
FileUtils.deleteDirectory(tempOutputDir.toFile());
}
} catch (Exception e) {
log.error("Error cleaning up temporary files", e);
}
} }
} }
@@ -185,7 +208,13 @@ public class ConvertImgPDFController {
String fitOption = request.getFitOption(); String fitOption = request.getFitOption();
String colorType = request.getColorType(); String colorType = request.getColorType();
boolean autoRotate = request.isAutoRotate(); boolean autoRotate = request.isAutoRotate();
// Handle Null entries for formdata
if (colorType == null || colorType.isBlank()) {
colorType = "color";
}
if (fitOption == null || fitOption.isEmpty()) {
fitOption = "fillPage";
}
// Convert the file to PDF and get the resulting bytes // Convert the file to PDF and get the resulting bytes
byte[] bytes = byte[] bytes =
PdfUtils.imageToPdf(file, fitOption, autoRotate, colorType, pdfDocumentFactory); PdfUtils.imageToPdf(file, fitOption, autoRotate, colorType, pdfDocumentFactory);

View File

@@ -8,8 +8,6 @@ import java.util.Arrays;
import java.util.List; import java.util.List;
import org.apache.commons.io.FileUtils; import org.apache.commons.io.FileUtils;
import org.slf4j.Logger;
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;
@@ -22,6 +20,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 lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.api.converters.PdfToPdfARequest; import stirling.software.SPDF.model.api.converters.PdfToPdfARequest;
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;
@@ -29,11 +28,10 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@RequestMapping("/api/v1/convert") @RequestMapping("/api/v1/convert")
@Slf4j
@Tag(name = "Convert", description = "Convert APIs") @Tag(name = "Convert", description = "Convert APIs")
public class ConvertPDFToPDFA { public class ConvertPDFToPDFA {
private static final Logger logger = LoggerFactory.getLogger(ConvertPDFToPDFA.class);
@PostMapping(consumes = "multipart/form-data", value = "/pdf/pdfa") @PostMapping(consumes = "multipart/form-data", value = "/pdf/pdfa")
@Operation( @Operation(
summary = "Convert a PDF to a PDF/A", summary = "Convert a PDF to a PDF/A",
@@ -46,7 +44,7 @@ public class ConvertPDFToPDFA {
// Validate input file type // Validate input file type
if (!"application/pdf".equals(inputFile.getContentType())) { if (!"application/pdf".equals(inputFile.getContentType())) {
logger.error("Invalid input file type: {}", inputFile.getContentType()); log.error("Invalid input file type: {}", inputFile.getContentType());
throw new IllegalArgumentException("Input file must be a PDF"); throw new IllegalArgumentException("Input file must be a PDF");
} }
@@ -96,7 +94,7 @@ public class ConvertPDFToPDFA {
.runCommandWithOutputHandling(command); .runCommandWithOutputHandling(command);
if (returnCode.getRc() != 0) { if (returnCode.getRc() != 0) {
logger.error("PDF/A conversion failed with return code: {}", returnCode.getRc()); log.error("PDF/A conversion failed with return code: {}", returnCode.getRc());
throw new RuntimeException("PDF/A conversion failed"); throw new RuntimeException("PDF/A conversion failed");
} }

View File

@@ -7,8 +7,6 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocument;
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.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.ModelAttribute;
@@ -19,6 +17,7 @@ import org.springframework.web.bind.annotation.RestController;
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 lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.api.converters.UrlToPdfRequest; import stirling.software.SPDF.model.api.converters.UrlToPdfRequest;
import stirling.software.SPDF.service.CustomPDDocumentFactory; import stirling.software.SPDF.service.CustomPDDocumentFactory;
import stirling.software.SPDF.utils.GeneralUtils; import stirling.software.SPDF.utils.GeneralUtils;
@@ -28,11 +27,10 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@Tag(name = "Convert", description = "Convert APIs") @Tag(name = "Convert", description = "Convert APIs")
@Slf4j
@RequestMapping("/api/v1/convert") @RequestMapping("/api/v1/convert")
public class ConvertWebsiteToPDF { public class ConvertWebsiteToPDF {
private static final Logger logger = LoggerFactory.getLogger(ConvertWebsiteToPDF.class);
private final CustomPDDocumentFactory pdfDocumentFactory; private final CustomPDDocumentFactory pdfDocumentFactory;
@Autowired @Autowired
@@ -88,7 +86,7 @@ public class ConvertWebsiteToPDF {
try { try {
Files.deleteIfExists(tempOutputFile); Files.deleteIfExists(tempOutputFile);
} catch (IOException e) { } catch (IOException e) {
logger.error("Error deleting temporary output file", e); log.error("Error deleting temporary output file", e);
} }
} }
} }

View File

@@ -7,8 +7,6 @@ import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.QuoteMode; import org.apache.commons.csv.QuoteMode;
import org.apache.pdfbox.Loader; import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocument;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ContentDisposition; import org.springframework.http.ContentDisposition;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
@@ -34,8 +32,6 @@ import technology.tabula.writers.Writer;
@Tag(name = "Convert", description = "Convert APIs") @Tag(name = "Convert", description = "Convert APIs")
public class ExtractCSVController { public class ExtractCSVController {
private static final Logger logger = LoggerFactory.getLogger(ExtractCSVController.class);
@PostMapping(value = "/pdf/csv", consumes = "multipart/form-data") @PostMapping(value = "/pdf/csv", consumes = "multipart/form-data")
@Operation( @Operation(
summary = "Extracts a CSV document from a PDF", summary = "Extracts a CSV document from a PDF",

View File

@@ -9,8 +9,6 @@ import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.text.PDFTextStripper; import org.apache.pdfbox.text.PDFTextStripper;
import org.apache.pdfbox.text.TextPosition; import org.apache.pdfbox.text.TextPosition;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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;
@@ -22,16 +20,16 @@ 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 lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.api.misc.ExtractHeaderRequest; import stirling.software.SPDF.model.api.misc.ExtractHeaderRequest;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@RequestMapping("/api/v1/misc") @RequestMapping("/api/v1/misc")
@Slf4j
@Tag(name = "Misc", description = "Miscellaneous APIs") @Tag(name = "Misc", description = "Miscellaneous APIs")
public class AutoRenameController { public class AutoRenameController {
private static final Logger logger = LoggerFactory.getLogger(AutoRenameController.class);
private static final float TITLE_FONT_SIZE_THRESHOLD = 20.0f; private static final float TITLE_FONT_SIZE_THRESHOLD = 20.0f;
private static final int LINE_LIMIT = 200; private static final int LINE_LIMIT = 200;
@@ -133,7 +131,7 @@ public class AutoRenameController {
header = header.replaceAll("[/\\\\?%*:|\"<>]", "").trim(); header = header.replaceAll("[/\\\\?%*:|\"<>]", "").trim();
return WebResponseUtils.pdfDocToWebResponse(document, header + ".pdf"); return WebResponseUtils.pdfDocToWebResponse(document, header + ".pdf");
} else { } else {
logger.info("File has no good title to be found"); log.info("File has no good title to be found");
return WebResponseUtils.pdfDocToWebResponse( return WebResponseUtils.pdfDocToWebResponse(
document, Filenames.toSimpleFileName(file.getOriginalFilename())); document, Filenames.toSimpleFileName(file.getOriginalFilename()));
} }

View File

@@ -14,8 +14,6 @@ import java.util.zip.ZipOutputStream;
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.rendering.PDFRenderer; import org.apache.pdfbox.rendering.PDFRenderer;
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.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
@@ -37,16 +35,17 @@ 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 lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.api.misc.AutoSplitPdfRequest; import stirling.software.SPDF.model.api.misc.AutoSplitPdfRequest;
import stirling.software.SPDF.service.CustomPDDocumentFactory; import stirling.software.SPDF.service.CustomPDDocumentFactory;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@RequestMapping("/api/v1/misc") @RequestMapping("/api/v1/misc")
@Slf4j
@Tag(name = "Misc", description = "Miscellaneous APIs") @Tag(name = "Misc", description = "Miscellaneous APIs")
public class AutoSplitPdfController { public class AutoSplitPdfController {
private static final Logger logger = LoggerFactory.getLogger(AutoSplitPdfController.class);
private static final String QR_CONTENT = "https://github.com/Stirling-Tools/Stirling-PDF"; private static final String QR_CONTENT = "https://github.com/Stirling-Tools/Stirling-PDF";
private static final String QR_CONTENT_OLD = "https://github.com/Frooodle/Stirling-PDF"; private static final String QR_CONTENT_OLD = "https://github.com/Frooodle/Stirling-PDF";
@@ -134,7 +133,7 @@ public class AutoSplitPdfController {
try { try {
document.close(); document.close();
} catch (IOException e) { } catch (IOException e) {
logger.error("Error closing main PDDocument", e); log.error("Error closing main PDDocument", e);
} }
} }
@@ -142,7 +141,7 @@ public class AutoSplitPdfController {
try { try {
splitDoc.close(); splitDoc.close();
} catch (IOException e) { } catch (IOException e) {
logger.error("Error closing split PDDocument", e); log.error("Error closing split PDDocument", e);
} }
} }
@@ -150,7 +149,7 @@ public class AutoSplitPdfController {
try { try {
Files.deleteIfExists(zipFile); Files.deleteIfExists(zipFile);
} catch (IOException e) { } catch (IOException e) {
logger.error("Error deleting temporary zip file", e); log.error("Error deleting temporary zip file", e);
} }
} }
} }

View File

@@ -14,8 +14,6 @@ import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageTree; import org.apache.pdfbox.pdmodel.PDPageTree;
import org.apache.pdfbox.rendering.PDFRenderer; import org.apache.pdfbox.rendering.PDFRenderer;
import org.apache.pdfbox.text.PDFTextStripper; import org.apache.pdfbox.text.PDFTextStripper;
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.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
@@ -30,6 +28,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 lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.api.misc.RemoveBlankPagesRequest; import stirling.software.SPDF.model.api.misc.RemoveBlankPagesRequest;
import stirling.software.SPDF.service.CustomPDDocumentFactory; import stirling.software.SPDF.service.CustomPDDocumentFactory;
import stirling.software.SPDF.utils.PdfUtils; import stirling.software.SPDF.utils.PdfUtils;
@@ -37,11 +36,10 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@RequestMapping("/api/v1/misc") @RequestMapping("/api/v1/misc")
@Slf4j
@Tag(name = "Misc", description = "Miscellaneous APIs") @Tag(name = "Misc", description = "Miscellaneous APIs")
public class BlankPageController { public class BlankPageController {
private static final Logger logger = LoggerFactory.getLogger(BlankPageController.class);
private final CustomPDDocumentFactory pdfDocumentFactory; private final CustomPDDocumentFactory pdfDocumentFactory;
@Autowired @Autowired
@@ -71,7 +69,7 @@ public class BlankPageController {
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); log.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);
@@ -79,12 +77,12 @@ public class BlankPageController {
boolean blank = true; boolean blank = true;
if (hasText) { if (hasText) {
logger.info("page {} has text, not blank", pageIndex); log.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 {} has image, running blank detection", pageIndex); log.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);
@@ -92,10 +90,10 @@ public class BlankPageController {
} }
if (blank) { if (blank) {
logger.info("Skipping, Image was blank for page #{}", pageIndex); log.info("Skipping, Image was blank for page #{}", pageIndex);
blankPages.add(page); blankPages.add(page);
} else { } else {
logger.info("page {} has image which is not blank", pageIndex); log.info("page {} has image which is not blank", pageIndex);
nonBlankPages.add(page); nonBlankPages.add(page);
} }
@@ -121,12 +119,12 @@ public class BlankPageController {
zos.close(); zos.close();
logger.info("Returning ZIP file: {}", filename + "_processed.zip"); log.info("Returning ZIP file: {}", filename + "_processed.zip");
return WebResponseUtils.boasToWebResponse( return WebResponseUtils.boasToWebResponse(
baos, filename + "_processed.zip", MediaType.APPLICATION_OCTET_STREAM); baos, filename + "_processed.zip", MediaType.APPLICATION_OCTET_STREAM);
} catch (IOException e) { } catch (IOException e) {
logger.error("exception", e); log.error("exception", e);
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
} }
} }
@@ -149,7 +147,7 @@ public class BlankPageController {
public static boolean isBlankImage( public static boolean isBlankImage(
BufferedImage image, int threshold, double whitePercent, int blurSize) { BufferedImage image, int threshold, double whitePercent, int blurSize) {
if (image == null) { if (image == null) {
logger.info("Error: Image is null"); log.info("Error: Image is null");
return false; return false;
} }
@@ -167,7 +165,7 @@ public class BlankPageController {
} }
double whitePixelPercentage = (whitePixels / (double) totalPixels) * 100; double whitePixelPercentage = (whitePixels / (double) totalPixels) * 100;
logger.info(String.format("Page has white pixel percent of %.2f%%", whitePixelPercentage)); log.info(String.format("Page has white pixel percent of %.2f%%", whitePixelPercentage));
return whitePixelPercentage >= whitePercent; return whitePixelPercentage >= whitePercent;
} }

View File

@@ -17,8 +17,6 @@ import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDResources; import org.apache.pdfbox.pdmodel.PDResources;
import org.apache.pdfbox.pdmodel.graphics.PDXObject; import org.apache.pdfbox.pdmodel.graphics.PDXObject;
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
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.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.ModelAttribute;
@@ -31,6 +29,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 lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.api.misc.OptimizePdfRequest; import stirling.software.SPDF.model.api.misc.OptimizePdfRequest;
import stirling.software.SPDF.service.CustomPDDocumentFactory; import stirling.software.SPDF.service.CustomPDDocumentFactory;
import stirling.software.SPDF.utils.GeneralUtils; import stirling.software.SPDF.utils.GeneralUtils;
@@ -40,11 +39,10 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@RequestMapping("/api/v1/misc") @RequestMapping("/api/v1/misc")
@Slf4j
@Tag(name = "Misc", description = "Miscellaneous APIs") @Tag(name = "Misc", description = "Miscellaneous APIs")
public class CompressController { public class CompressController {
private static final Logger logger = LoggerFactory.getLogger(CompressController.class);
private final CustomPDDocumentFactory pdfDocumentFactory; private final CustomPDDocumentFactory pdfDocumentFactory;
@Autowired @Autowired
@@ -191,7 +189,7 @@ public class CompressController {
incrementOptimizeLevel( incrementOptimizeLevel(
optimizeLevel, outputFileSize, expectedOutputSize); optimizeLevel, outputFileSize, expectedOutputSize);
if (autoMode && optimizeLevel > 9) { if (autoMode && optimizeLevel > 9) {
logger.info("Maximum compression level reached in auto mode"); log.info("Maximum compression level reached in auto mode");
sizeMet = true; sizeMet = true;
} }
} }
@@ -203,7 +201,7 @@ public class CompressController {
// Check if optimized file is larger than the original // Check if optimized file is larger than the original
if (pdfBytes.length > inputFileSize) { if (pdfBytes.length > inputFileSize) {
logger.warn( log.warn(
"Optimized file is larger than the original. Returning the original file instead."); "Optimized file is larger than the original. Returning the original file instead.");
finalFile = tempInputFile; finalFile = tempInputFile;
} }
@@ -234,7 +232,7 @@ public class CompressController {
private int incrementOptimizeLevel(int currentLevel, long currentSize, long targetSize) { private int incrementOptimizeLevel(int currentLevel, long currentSize, long targetSize) {
double currentRatio = currentSize / (double) targetSize; double currentRatio = currentSize / (double) targetSize;
logger.info("Current compression ratio: {}", String.format("%.2f", currentRatio)); log.info("Current compression ratio: {}", String.format("%.2f", currentRatio));
if (currentRatio > 2.0) { if (currentRatio > 2.0) {
return Math.min(9, currentLevel + 3); return Math.min(9, currentLevel + 3);

View File

@@ -17,8 +17,6 @@ import org.apache.commons.io.FileUtils;
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.rendering.PDFRenderer; import org.apache.pdfbox.rendering.PDFRenderer;
import org.slf4j.Logger;
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.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
@@ -31,6 +29,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody; import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.api.misc.ExtractImageScansRequest; import stirling.software.SPDF.model.api.misc.ExtractImageScansRequest;
import stirling.software.SPDF.utils.CheckProgramInstall; import stirling.software.SPDF.utils.CheckProgramInstall;
import stirling.software.SPDF.utils.ProcessExecutor; import stirling.software.SPDF.utils.ProcessExecutor;
@@ -39,11 +38,10 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@RequestMapping("/api/v1/misc") @RequestMapping("/api/v1/misc")
@Slf4j
@Tag(name = "Misc", description = "Miscellaneous APIs") @Tag(name = "Misc", description = "Miscellaneous APIs")
public class ExtractImageScansController { public class ExtractImageScansController {
private static final Logger logger = LoggerFactory.getLogger(ExtractImageScansController.class);
@PostMapping(consumes = "multipart/form-data", value = "/extract-image-scans") @PostMapping(consumes = "multipart/form-data", value = "/extract-image-scans")
@Operation( @Operation(
summary = "Extract image scans from an input file", summary = "Extract image scans from an input file",
@@ -201,7 +199,7 @@ public class ExtractImageScansController {
try { try {
Files.deleteIfExists(path); Files.deleteIfExists(path);
} catch (IOException e) { } catch (IOException e) {
logger.error("Failed to delete temporary image file: " + path, e); log.error("Failed to delete temporary image file: " + path, e);
} }
}); });
@@ -209,7 +207,7 @@ public class ExtractImageScansController {
try { try {
Files.deleteIfExists(tempZipFile); Files.deleteIfExists(tempZipFile);
} catch (IOException e) { } catch (IOException e) {
logger.error("Failed to delete temporary zip file: " + tempZipFile, e); log.error("Failed to delete temporary zip file: " + tempZipFile, e);
} }
} }
@@ -218,7 +216,7 @@ public class ExtractImageScansController {
try { try {
FileUtils.deleteDirectory(dir.toFile()); FileUtils.deleteDirectory(dir.toFile());
} catch (IOException e) { } catch (IOException e) {
logger.error("Failed to delete temporary directory: " + dir, e); log.error("Failed to delete temporary directory: " + dir, e);
} }
}); });
} }

View File

@@ -25,8 +25,6 @@ import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
import org.slf4j.Logger;
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;
@@ -39,17 +37,17 @@ 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 lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.api.PDFExtractImagesRequest; import stirling.software.SPDF.model.api.PDFExtractImagesRequest;
import stirling.software.SPDF.utils.ImageProcessingUtils; import stirling.software.SPDF.utils.ImageProcessingUtils;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@RequestMapping("/api/v1/misc") @RequestMapping("/api/v1/misc")
@Slf4j
@Tag(name = "Misc", description = "Miscellaneous APIs") @Tag(name = "Misc", description = "Miscellaneous APIs")
public class ExtractImagesController { public class ExtractImagesController {
private static final Logger logger = LoggerFactory.getLogger(ExtractImagesController.class);
@PostMapping(consumes = "multipart/form-data", value = "/extract-images") @PostMapping(consumes = "multipart/form-data", value = "/extract-images")
@Operation( @Operation(
summary = "Extract images from a PDF file", summary = "Extract images from a PDF file",
@@ -107,7 +105,7 @@ public class ExtractImagesController {
allowDuplicates); allowDuplicates);
} catch (IOException e) { } catch (IOException e) {
// Log the error and continue processing other pages // Log the error and continue processing other pages
logger.error( log.error(
"Error extracting images from page {}: {}", "Error extracting images from page {}: {}",
pageNum, pageNum,
e.getMessage()); e.getMessage());
@@ -167,7 +165,7 @@ public class ExtractImagesController {
try { try {
md = MessageDigest.getInstance("MD5"); md = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException e) { } catch (NoSuchAlgorithmException e) {
logger.error("MD5 algorithm not available for extractImages hash.", e); log.error("MD5 algorithm not available for extractImages hash.", e);
return; return;
} }
if (page.getResources() == null || page.getResources().getXObjectNames() == null) { if (page.getResources() == null || page.getResources().getXObjectNames() == null) {

View File

@@ -27,8 +27,6 @@ import org.apache.pdfbox.pdmodel.graphics.image.JPEGFactory;
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
import org.apache.pdfbox.rendering.ImageType; import org.apache.pdfbox.rendering.ImageType;
import org.apache.pdfbox.rendering.PDFRenderer; import org.apache.pdfbox.rendering.PDFRenderer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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,8 +48,6 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@Tag(name = "Misc", description = "Miscellaneous APIs") @Tag(name = "Misc", description = "Miscellaneous APIs")
public class FakeScanControllerWIP { public class FakeScanControllerWIP {
private static final Logger logger = LoggerFactory.getLogger(FakeScanControllerWIP.class);
// TODO finish // TODO finish
@PostMapping(consumes = "multipart/form-data", value = "/fake-scan") @PostMapping(consumes = "multipart/form-data", value = "/fake-scan")
@Hidden @Hidden

View File

@@ -12,8 +12,6 @@ import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm; import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
import org.apache.pdfbox.rendering.ImageType; import org.apache.pdfbox.rendering.ImageType;
import org.apache.pdfbox.rendering.PDFRenderer; import org.apache.pdfbox.rendering.PDFRenderer;
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.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.ModelAttribute;
@@ -26,17 +24,17 @@ 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 lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.api.misc.FlattenRequest; import stirling.software.SPDF.model.api.misc.FlattenRequest;
import stirling.software.SPDF.service.CustomPDDocumentFactory; import stirling.software.SPDF.service.CustomPDDocumentFactory;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@RequestMapping("/api/v1/misc") @RequestMapping("/api/v1/misc")
@Slf4j
@Tag(name = "Misc", description = "Miscellaneous APIs") @Tag(name = "Misc", description = "Miscellaneous APIs")
public class FlattenController { public class FlattenController {
private static final Logger logger = LoggerFactory.getLogger(FlattenController.class);
private final CustomPDDocumentFactory pdfDocumentFactory; private final CustomPDDocumentFactory pdfDocumentFactory;
@Autowired @Autowired
@@ -84,7 +82,7 @@ public class FlattenController {
contentStream.drawImage(pdImage, 0, 0, pageWidth, pageHeight); contentStream.drawImage(pdImage, 0, 0, pageWidth, pageHeight);
} }
} catch (IOException e) { } catch (IOException e) {
logger.error("exception", e); log.error("exception", e);
} }
} }
return WebResponseUtils.pdfDocToWebResponse( return WebResponseUtils.pdfDocToWebResponse(

View File

@@ -11,9 +11,9 @@ import org.apache.pdfbox.Loader;
import org.apache.pdfbox.cos.COSName; import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDDocumentInformation; import org.apache.pdfbox.pdmodel.PDDocumentInformation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
@@ -24,16 +24,17 @@ 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 lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.api.misc.MetadataRequest; import stirling.software.SPDF.model.api.misc.MetadataRequest;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
import stirling.software.SPDF.utils.propertyeditor.StringToMapPropertyEditor;
@RestController @RestController
@RequestMapping("/api/v1/misc") @RequestMapping("/api/v1/misc")
@Slf4j
@Tag(name = "Misc", description = "Miscellaneous APIs") @Tag(name = "Misc", description = "Miscellaneous APIs")
public class MetadataController { public class MetadataController {
private static final Logger logger = LoggerFactory.getLogger(MetadataController.class);
private String checkUndefined(String entry) { private String checkUndefined(String entry) {
// Check if the string is "undefined" // Check if the string is "undefined"
if ("undefined".equals(entry)) { if ("undefined".equals(entry)) {
@@ -44,6 +45,11 @@ public class MetadataController {
return entry; return entry;
} }
@InitBinder
public void initBinder(WebDataBinder binder) {
binder.registerCustomEditor(Map.class, "allRequestParams", new StringToMapPropertyEditor());
}
@PostMapping(consumes = "multipart/form-data", value = "/update-metadata") @PostMapping(consumes = "multipart/form-data", value = "/update-metadata")
@Operation( @Operation(
summary = "Update metadata of a PDF file", summary = "Update metadata of a PDF file",
@@ -140,7 +146,7 @@ public class MetadataController {
creationDateCal.setTime( creationDateCal.setTime(
new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").parse(creationDate)); new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").parse(creationDate));
} catch (ParseException e) { } catch (ParseException e) {
logger.error("exception", e); log.error("exception", e);
} }
info.setCreationDate(creationDateCal); info.setCreationDate(creationDateCal);
} else { } else {
@@ -152,7 +158,7 @@ public class MetadataController {
modificationDateCal.setTime( modificationDateCal.setTime(
new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").parse(modificationDate)); new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").parse(modificationDate));
} catch (ParseException e) { } catch (ParseException e) {
logger.error("exception", e); log.error("exception", e);
} }
info.setModificationDate(modificationDateCal); info.setModificationDate(modificationDateCal);
} else { } else {

View File

@@ -87,7 +87,7 @@ public class OCRController {
Files.createDirectories(tempOutputDir); Files.createDirectories(tempOutputDir);
Files.createDirectories(tempImagesDir); Files.createDirectories(tempImagesDir);
Process process = null;
try { try {
// Save input file // Save input file
inputFile.transferTo(tempInputFile.toFile()); inputFile.transferTo(tempInputFile.toFile());
@@ -139,7 +139,7 @@ public class OCRController {
command.add("pdf"); // Always output PDF command.add("pdf"); // Always output PDF
ProcessBuilder pb = new ProcessBuilder(command); ProcessBuilder pb = new ProcessBuilder(command);
Process process = pb.start(); process = pb.start();
// Capture any error output // Capture any error output
try (BufferedReader reader = try (BufferedReader reader =
@@ -188,6 +188,10 @@ public class OCRController {
.body(pdfContent); .body(pdfContent);
} finally { } finally {
if (process != null) {
process.destroy();
}
// Clean up temporary files // Clean up temporary files
deleteDirectory(tempDir); deleteDirectory(tempDir);
} }

View File

@@ -2,8 +2,6 @@ package stirling.software.SPDF.controller.api.misc;
import java.io.IOException; import java.io.IOException;
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.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
@@ -17,6 +15,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 lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.api.misc.OverlayImageRequest; import stirling.software.SPDF.model.api.misc.OverlayImageRequest;
import stirling.software.SPDF.service.CustomPDDocumentFactory; import stirling.software.SPDF.service.CustomPDDocumentFactory;
import stirling.software.SPDF.utils.PdfUtils; import stirling.software.SPDF.utils.PdfUtils;
@@ -24,11 +23,10 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@RequestMapping("/api/v1/misc") @RequestMapping("/api/v1/misc")
@Slf4j
@Tag(name = "Misc", description = "Miscellaneous APIs") @Tag(name = "Misc", description = "Miscellaneous APIs")
public class OverlayImageController { public class OverlayImageController {
private static final Logger logger = LoggerFactory.getLogger(OverlayImageController.class);
private final CustomPDDocumentFactory pdfDocumentFactory; private final CustomPDDocumentFactory pdfDocumentFactory;
@Autowired @Autowired
@@ -60,7 +58,7 @@ public class OverlayImageController {
.replaceFirst("[.][^.]+$", "") .replaceFirst("[.][^.]+$", "")
+ "_overlayed.pdf"); + "_overlayed.pdf");
} catch (IOException e) { } catch (IOException e) {
logger.error("Failed to add image to PDF", e); log.error("Failed to add image to PDF", e);
return new ResponseEntity<>(HttpStatus.BAD_REQUEST); return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
} }
} }

View File

@@ -10,8 +10,6 @@ import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.common.PDRectangle; import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.font.PDType1Font; import org.apache.pdfbox.pdmodel.font.PDType1Font;
import org.apache.pdfbox.pdmodel.font.Standard14Fonts; import org.apache.pdfbox.pdmodel.font.Standard14Fonts;
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.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
@@ -35,8 +33,6 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@Tag(name = "Misc", description = "Miscellaneous APIs") @Tag(name = "Misc", description = "Miscellaneous APIs")
public class PageNumbersController { public class PageNumbersController {
private static final Logger logger = LoggerFactory.getLogger(PageNumbersController.class);
private final CustomPDDocumentFactory pdfDocumentFactory; private final CustomPDDocumentFactory pdfDocumentFactory;
@Autowired @Autowired

View File

@@ -6,8 +6,6 @@ import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
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.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.ModelAttribute;
@@ -31,8 +29,6 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@Tag(name = "Misc", description = "Miscellaneous APIs") @Tag(name = "Misc", description = "Miscellaneous APIs")
public class RepairController { public class RepairController {
private static final Logger logger = LoggerFactory.getLogger(RepairController.class);
private final CustomPDDocumentFactory pdfDocumentFactory; private final CustomPDDocumentFactory pdfDocumentFactory;
@Autowired @Autowired

View File

@@ -7,8 +7,6 @@ import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.common.PDNameTreeNode; import org.apache.pdfbox.pdmodel.common.PDNameTreeNode;
import org.apache.pdfbox.pdmodel.interactive.action.PDActionJavaScript; import org.apache.pdfbox.pdmodel.interactive.action.PDActionJavaScript;
import org.slf4j.Logger;
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;
@@ -29,8 +27,6 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@Tag(name = "Misc", description = "Miscellaneous APIs") @Tag(name = "Misc", description = "Miscellaneous APIs")
public class ShowJavascript { public class ShowJavascript {
private static final Logger logger = LoggerFactory.getLogger(ShowJavascript.class);
@PostMapping(consumes = "multipart/form-data", value = "/show-javascript") @PostMapping(consumes = "multipart/form-data", value = "/show-javascript")
@Operation( @Operation(
summary = "Grabs all JS from a PDF and returns a single JS file with all code", summary = "Grabs all JS from a PDF and returns a single JS file with all code",

View File

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

View File

@@ -7,8 +7,6 @@ import java.util.Map;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
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.http.HttpEntity; import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
@@ -21,17 +19,17 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.ServletContext; import jakarta.servlet.ServletContext;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.SPdfApplication; import stirling.software.SPDF.SPdfApplication;
import stirling.software.SPDF.model.ApiEndpoint; import stirling.software.SPDF.model.ApiEndpoint;
import stirling.software.SPDF.model.Role; import stirling.software.SPDF.model.Role;
@Service @Service
@Slf4j
public class ApiDocService { public class ApiDocService {
private final Map<String, ApiEndpoint> apiDocumentation = new HashMap<>(); private final Map<String, ApiEndpoint> apiDocumentation = new HashMap<>();
private static final Logger logger = LoggerFactory.getLogger(ApiDocService.class);
@Autowired private ServletContext servletContext; @Autowired private ServletContext servletContext;
private String getApiDocsUrl() { private String getApiDocsUrl() {
@@ -135,7 +133,7 @@ public class ApiDocService {
}); });
} catch (Exception e) { } catch (Exception e) {
// Handle exceptions // Handle exceptions
logger.error("Error grabbing swagger doc, body result {}", apiDocsJson); log.error("Error grabbing swagger doc, body result {}", apiDocsJson);
} }
} }

View File

@@ -8,8 +8,6 @@ import java.util.Map;
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.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
@@ -26,6 +24,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.PipelineConfig; import stirling.software.SPDF.model.PipelineConfig;
import stirling.software.SPDF.model.api.HandleDataRequest; import stirling.software.SPDF.model.api.HandleDataRequest;
@@ -33,11 +32,10 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@RequestMapping("/api/v1/pipeline") @RequestMapping("/api/v1/pipeline")
@Slf4j
@Tag(name = "Pipeline", description = "Pipeline APIs") @Tag(name = "Pipeline", description = "Pipeline APIs")
public class PipelineController { public class PipelineController {
private static final Logger logger = LoggerFactory.getLogger(PipelineController.class);
final String watchedFoldersDir = "./pipeline/watchedFolders/"; final String watchedFoldersDir = "./pipeline/watchedFolders/";
final String finishedFoldersDir = "./pipeline/finishedFolders/"; final String finishedFoldersDir = "./pipeline/finishedFolders/";
@Autowired PipelineProcessor processor; @Autowired PipelineProcessor processor;
@@ -56,7 +54,7 @@ public class PipelineController {
return null; return null;
} }
PipelineConfig config = objectMapper.readValue(jsonString, PipelineConfig.class); PipelineConfig config = objectMapper.readValue(jsonString, PipelineConfig.class);
logger.info("Received POST request to /handleData with {} files", files.length); log.info("Received POST request to /handleData with {} files", files.length);
try { try {
List<Resource> inputFiles = processor.generateInputFiles(files); List<Resource> inputFiles = processor.generateInputFiles(files);
if (inputFiles == null || inputFiles.size() == 0) { if (inputFiles == null || inputFiles.size() == 0) {
@@ -71,7 +69,7 @@ public class PipelineController {
is.read(bytes); is.read(bytes);
is.close(); is.close();
logger.info("Returning single file response..."); log.info("Returning single file response...");
return WebResponseUtils.bytesToWebResponse( return WebResponseUtils.bytesToWebResponse(
bytes, singleFile.getFilename(), MediaType.APPLICATION_OCTET_STREAM); bytes, singleFile.getFilename(), MediaType.APPLICATION_OCTET_STREAM);
} else if (outputFiles == null) { } else if (outputFiles == null) {
@@ -118,11 +116,11 @@ public class PipelineController {
zipOut.close(); zipOut.close();
logger.info("Returning zipped file response..."); log.info("Returning zipped file response...");
return WebResponseUtils.boasToWebResponse( return WebResponseUtils.boasToWebResponse(
baos, "output.zip", MediaType.APPLICATION_OCTET_STREAM); baos, "output.zip", MediaType.APPLICATION_OCTET_STREAM);
} catch (Exception e) { } catch (Exception e) {
logger.error("Error handling data: ", e); log.error("Error handling data: ", e);
return null; return null;
} }
} }

View File

@@ -16,8 +16,6 @@ import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Stream; import java.util.stream.Stream;
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.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.io.ByteArrayResource; import org.springframework.core.io.ByteArrayResource;
@@ -27,14 +25,15 @@ import org.springframework.stereotype.Service;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.PipelineConfig; import stirling.software.SPDF.model.PipelineConfig;
import stirling.software.SPDF.model.PipelineOperation; import stirling.software.SPDF.model.PipelineOperation;
import stirling.software.SPDF.utils.FileMonitor; import stirling.software.SPDF.utils.FileMonitor;
@Service @Service
@Slf4j
public class PipelineDirectoryProcessor { public class PipelineDirectoryProcessor {
private static final Logger logger = LoggerFactory.getLogger(PipelineDirectoryProcessor.class);
@Autowired private ObjectMapper objectMapper; @Autowired private ObjectMapper objectMapper;
@Autowired private ApiDocService apiDocService; @Autowired private ApiDocService apiDocService;
@Autowired PipelineProcessor processor; @Autowired PipelineProcessor processor;
@@ -56,9 +55,9 @@ public class PipelineDirectoryProcessor {
if (!Files.exists(watchedFolderPath)) { if (!Files.exists(watchedFolderPath)) {
try { try {
Files.createDirectories(watchedFolderPath); Files.createDirectories(watchedFolderPath);
logger.info("Created directory: {}", watchedFolderPath); log.info("Created directory: {}", watchedFolderPath);
} catch (IOException e) { } catch (IOException e) {
logger.error("Error creating directory: {}", watchedFolderPath, e); log.error("Error creating directory: {}", watchedFolderPath, e);
return; return;
} }
} }
@@ -71,21 +70,21 @@ public class PipelineDirectoryProcessor {
handleDirectory(t); handleDirectory(t);
} }
} catch (Exception e) { } catch (Exception e) {
logger.error("Error handling directory: {}", t, e); log.error("Error handling directory: {}", t, e);
} }
}); });
} catch (Exception e) { } catch (Exception e) {
logger.error("Error walking through directory: {}", watchedFolderPath, e); log.error("Error walking through directory: {}", watchedFolderPath, e);
} }
} }
public void handleDirectory(Path dir) throws IOException { public void handleDirectory(Path dir) throws IOException {
logger.info("Handling directory: {}", dir); log.info("Handling directory: {}", dir);
Path processingDir = createProcessingDirectory(dir); Path processingDir = createProcessingDirectory(dir);
Optional<Path> jsonFileOptional = findJsonFile(dir); Optional<Path> jsonFileOptional = findJsonFile(dir);
if (!jsonFileOptional.isPresent()) { if (!jsonFileOptional.isPresent()) {
logger.warn("No .JSON settings file found. No processing will happen for dir {}.", dir); log.warn("No .JSON settings file found. No processing will happen for dir {}.", dir);
return; return;
} }
@@ -98,7 +97,7 @@ public class PipelineDirectoryProcessor {
Path processingDir = dir.resolve("processing"); Path processingDir = dir.resolve("processing");
if (!Files.exists(processingDir)) { if (!Files.exists(processingDir)) {
Files.createDirectory(processingDir); Files.createDirectory(processingDir);
logger.info("Created processing directory: {}", processingDir); log.info("Created processing directory: {}", processingDir);
} }
return processingDir; return processingDir;
} }
@@ -111,7 +110,7 @@ public class PipelineDirectoryProcessor {
private PipelineConfig readAndParseJson(Path jsonFile) throws IOException { private PipelineConfig readAndParseJson(Path jsonFile) throws IOException {
String jsonString = new String(Files.readAllBytes(jsonFile), StandardCharsets.UTF_8); String jsonString = new String(Files.readAllBytes(jsonFile), StandardCharsets.UTF_8);
logger.debug("Reading JSON file: {}", jsonFile); log.debug("Reading JSON file: {}", jsonFile);
return objectMapper.readValue(jsonString, PipelineConfig.class); return objectMapper.readValue(jsonString, PipelineConfig.class);
} }
@@ -121,7 +120,7 @@ public class PipelineDirectoryProcessor {
validateOperation(operation); validateOperation(operation);
File[] files = collectFilesForProcessing(dir, jsonFile, operation); File[] files = collectFilesForProcessing(dir, jsonFile, operation);
if (files == null || files.length == 0) { if (files == null || files.length == 0) {
logger.debug("No files detected for {} ", dir); log.debug("No files detected for {} ", dir);
return; return;
} }
List<File> filesToProcess = prepareFilesForProcessing(files, processingDir); List<File> filesToProcess = prepareFilesForProcessing(files, processingDir);
@@ -202,7 +201,7 @@ public class PipelineDirectoryProcessor {
moveAndRenameFiles(outputFiles, config, dir); moveAndRenameFiles(outputFiles, config, dir);
deleteOriginalFiles(filesToProcess, processingDir); deleteOriginalFiles(filesToProcess, processingDir);
} catch (Exception e) { } catch (Exception e) {
logger.error("error during processing", e); log.error("error during processing", e);
moveFilesBack(filesToProcess, processingDir); moveFilesBack(filesToProcess, processingDir);
} }
} }
@@ -215,7 +214,7 @@ public class PipelineDirectoryProcessor {
if (!Files.exists(outputPath)) { if (!Files.exists(outputPath)) {
Files.createDirectories(outputPath); Files.createDirectories(outputPath);
logger.info("Created directory: {}", outputPath); log.info("Created directory: {}", outputPath);
} }
Path outputFile = outputPath.resolve(outputFileName); Path outputFile = outputPath.resolve(outputFileName);
@@ -223,7 +222,7 @@ public class PipelineDirectoryProcessor {
os.write(((ByteArrayResource) resource).getByteArray()); os.write(((ByteArrayResource) resource).getByteArray());
} }
logger.info("File moved and renamed to {}", outputFile); log.info("File moved and renamed to {}", outputFile);
} }
} }
@@ -264,7 +263,7 @@ public class PipelineDirectoryProcessor {
throws IOException { throws IOException {
for (File file : filesToProcess) { for (File file : filesToProcess) {
Files.deleteIfExists(processingDir.resolve(file.getName())); Files.deleteIfExists(processingDir.resolve(file.getName()));
logger.info("Deleted original file: {}", file.getName()); log.info("Deleted original file: {}", file.getName());
} }
} }
@@ -272,12 +271,12 @@ public class PipelineDirectoryProcessor {
for (File file : filesToProcess) { for (File file : filesToProcess) {
try { try {
Files.move(processingDir.resolve(file.getName()), file.toPath()); Files.move(processingDir.resolve(file.getName()), file.toPath());
logger.info( log.info(
"Moved file back to original location: {} , {}", "Moved file back to original location: {} , {}",
file.toPath(), file.toPath(),
file.getName()); file.getName());
} catch (IOException e) { } catch (IOException e) {
logger.error("Error moving file back to original location: {}", file.getName(), e); log.error("Error moving file back to original location: {}", file.getName(), e);
} }
} }
} }

View File

@@ -19,8 +19,6 @@ import java.util.stream.Collectors;
import java.util.zip.ZipEntry; import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream; import java.util.zip.ZipInputStream;
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.core.io.ByteArrayResource; import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
@@ -40,16 +38,16 @@ import io.github.pixee.security.Filenames;
import io.github.pixee.security.ZipSecurity; import io.github.pixee.security.ZipSecurity;
import jakarta.servlet.ServletContext; import jakarta.servlet.ServletContext;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.SPdfApplication; import stirling.software.SPDF.SPdfApplication;
import stirling.software.SPDF.model.PipelineConfig; import stirling.software.SPDF.model.PipelineConfig;
import stirling.software.SPDF.model.PipelineOperation; import stirling.software.SPDF.model.PipelineOperation;
import stirling.software.SPDF.model.Role; import stirling.software.SPDF.model.Role;
@Service @Service
@Slf4j
public class PipelineProcessor { public class PipelineProcessor {
private static final Logger logger = LoggerFactory.getLogger(PipelineProcessor.class);
@Autowired private ApiDocService apiDocService; @Autowired private ApiDocService apiDocService;
@Autowired(required = false) @Autowired(required = false)
@@ -81,7 +79,7 @@ public class PipelineProcessor {
String operation = pipelineOperation.getOperation(); String operation = pipelineOperation.getOperation();
boolean isMultiInputOperation = apiDocService.isMultiInput(operation); boolean isMultiInputOperation = apiDocService.isMultiInput(operation);
logger.info( log.info(
"Running operation: {} isMultiInputOperation {}", "Running operation: {} isMultiInputOperation {}",
operation, operation,
isMultiInputOperation); isMultiInputOperation);
@@ -124,7 +122,7 @@ public class PipelineProcessor {
if (operation.startsWith("filter-") if (operation.startsWith("filter-")
&& (response.getBody() == null && (response.getBody() == null
|| response.getBody().length == 0)) { || response.getBody().length == 0)) {
logger.info("Skipping file due to failing {}", operation); log.info("Skipping file due to failing {}", operation);
continue; continue;
} }
@@ -208,7 +206,7 @@ public class PipelineProcessor {
outputFiles = newOutputFiles; outputFiles = newOutputFiles;
} }
if (hasErrors) { if (hasErrors) {
logger.error("Errors occurred during processing. Log: {}", logStream.toString()); log.error("Errors occurred during processing. Log: {}", logStream.toString());
} }
return outputFiles; return outputFiles;
@@ -221,7 +219,7 @@ public class PipelineProcessor {
HttpHeaders headers = new HttpHeaders(); HttpHeaders headers = new HttpHeaders();
String apiKey = getApiKeyForUser(); String apiKey = getApiKeyForUser();
headers.add("X-API-Key", apiKey); headers.add("X-API-KEY", apiKey);
headers.setContentType(MediaType.MULTIPART_FORM_DATA); headers.setContentType(MediaType.MULTIPART_FORM_DATA);
// Create HttpEntity with the body and headers // Create HttpEntity with the body and headers
@@ -310,7 +308,7 @@ public class PipelineProcessor {
List<Resource> generateInputFiles(File[] files) throws Exception { List<Resource> generateInputFiles(File[] files) throws Exception {
if (files == null || files.length == 0) { if (files == null || files.length == 0) {
logger.info("No files"); log.info("No files");
return null; return null;
} }
@@ -318,7 +316,7 @@ public class PipelineProcessor {
for (File file : files) { for (File file : files) {
Path path = Paths.get(file.getAbsolutePath()); Path path = Paths.get(file.getAbsolutePath());
logger.info("Reading file: " + path); // debug statement log.info("Reading file: " + path); // debug statement
if (Files.exists(path)) { if (Files.exists(path)) {
Resource fileResource = Resource fileResource =
@@ -330,16 +328,16 @@ public class PipelineProcessor {
}; };
outputFiles.add(fileResource); outputFiles.add(fileResource);
} else { } else {
logger.info("File not found: " + path); log.info("File not found: " + path);
} }
} }
logger.info("Files successfully loaded. Starting processing..."); log.info("Files successfully loaded. Starting processing...");
return outputFiles; return outputFiles;
} }
List<Resource> generateInputFiles(MultipartFile[] files) throws Exception { List<Resource> generateInputFiles(MultipartFile[] files) throws Exception {
if (files == null || files.length == 0) { if (files == null || files.length == 0) {
logger.info("No files"); log.info("No files");
return null; return null;
} }
@@ -355,7 +353,7 @@ public class PipelineProcessor {
}; };
outputFiles.add(fileResource); outputFiles.add(fileResource);
} }
logger.info("Files successfully loaded. Starting processing..."); log.info("Files successfully loaded. Starting processing...");
return outputFiles; return outputFiles;
} }
@@ -369,7 +367,7 @@ public class PipelineProcessor {
} }
private List<Resource> unzip(byte[] data) throws IOException { private List<Resource> unzip(byte[] data) throws IOException {
logger.info("Unzipping data of length: {}", data.length); log.info("Unzipping data of length: {}", data.length);
List<Resource> unzippedFiles = new ArrayList<>(); List<Resource> unzippedFiles = new ArrayList<>();
try (ByteArrayInputStream bais = new ByteArrayInputStream(data); try (ByteArrayInputStream bais = new ByteArrayInputStream(data);
@@ -396,7 +394,7 @@ public class PipelineProcessor {
// If the unzipped file is a zip file, unzip it // If the unzipped file is a zip file, unzip it
if (isZip(baos.toByteArray())) { if (isZip(baos.toByteArray())) {
logger.info("File {} is a zip file. Unzipping...", filename); log.info("File {} is a zip file. Unzipping...", filename);
unzippedFiles.addAll(unzip(baos.toByteArray())); unzippedFiles.addAll(unzip(baos.toByteArray()));
} else { } else {
unzippedFiles.add(fileResource); unzippedFiles.add(fileResource);
@@ -404,7 +402,7 @@ public class PipelineProcessor {
} }
} }
logger.info("Unzipping completed. {} files were unzipped.", unzippedFiles.size()); log.info("Unzipping completed. {} files were unzipped.", unzippedFiles.size());
return unzippedFiles; return unzippedFiles;
} }
} }

View File

@@ -63,8 +63,6 @@ import org.bouncycastle.operator.InputDecryptorProvider;
import org.bouncycastle.operator.OperatorCreationException; import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo; import org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo;
import org.bouncycastle.pkcs.PKCSException; import org.bouncycastle.pkcs.PKCSException;
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.core.io.ClassPathResource; import org.springframework.core.io.ClassPathResource;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
@@ -78,17 +76,17 @@ 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 lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.api.security.SignPDFWithCertRequest; import stirling.software.SPDF.model.api.security.SignPDFWithCertRequest;
import stirling.software.SPDF.service.CustomPDDocumentFactory; import stirling.software.SPDF.service.CustomPDDocumentFactory;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@RequestMapping("/api/v1/security") @RequestMapping("/api/v1/security")
@Slf4j
@Tag(name = "Security", description = "Security APIs") @Tag(name = "Security", description = "Security APIs")
public class CertSignController { public class CertSignController {
private static final Logger logger = LoggerFactory.getLogger(CertSignController.class);
static { static {
Security.addProvider(new BouncyCastleProvider()); Security.addProvider(new BouncyCastleProvider());
} }
@@ -108,7 +106,7 @@ public class CertSignController {
logoFile = Files.createTempFile("signature", ".png").toFile(); logoFile = Files.createTempFile("signature", ".png").toFile();
FileUtils.copyInputStreamToFile(is, logoFile); FileUtils.copyInputStreamToFile(is, logoFile);
} catch (IOException e) { } catch (IOException e) {
logger.error("Failed to load image signature file"); log.error("Failed to load image signature file");
throw e; throw e;
} }
} }
@@ -212,7 +210,9 @@ public class CertSignController {
@Operation( @Operation(
summary = "Sign PDF with a Digital Certificate", summary = "Sign PDF with a Digital Certificate",
description = description =
"This endpoint accepts a PDF file, a digital certificate and related information to sign the PDF. It then returns the digitally signed PDF file. Input:PDF Output:PDF Type:SISO") "This endpoint accepts a PDF file, a digital certificate and related information to sign"
+ " the PDF. It then returns the digitally signed PDF file. Input:PDF Output:PDF"
+ " Type:SISO")
public ResponseEntity<byte[]> signPDFWithCert(@ModelAttribute SignPDFWithCertRequest request) public ResponseEntity<byte[]> signPDFWithCert(@ModelAttribute SignPDFWithCertRequest request)
throws Exception { throws Exception {
MultipartFile pdf = request.getFileInput(); MultipartFile pdf = request.getFileInput();
@@ -308,7 +308,7 @@ public class CertSignController {
} }
doc.saveIncremental(output); doc.saveIncremental(output);
} catch (Exception e) { } catch (Exception e) {
logger.error("exception", e); log.error("exception", e);
} }
} }

View File

@@ -56,8 +56,6 @@ import org.apache.xmpbox.XMPMetadata;
import org.apache.xmpbox.xml.DomXmpParser; import org.apache.xmpbox.xml.DomXmpParser;
import org.apache.xmpbox.xml.XmpParsingException; import org.apache.xmpbox.xml.XmpParsingException;
import org.apache.xmpbox.xml.XmpSerializer; import org.apache.xmpbox.xml.XmpSerializer;
import org.slf4j.Logger;
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;
@@ -73,16 +71,16 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
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 lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.api.PDFFile; import stirling.software.SPDF.model.api.PDFFile;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@RequestMapping("/api/v1/security") @RequestMapping("/api/v1/security")
@Slf4j
@Tag(name = "Security", description = "Security APIs") @Tag(name = "Security", description = "Security APIs")
public class GetInfoOnPDF { public class GetInfoOnPDF {
private static final Logger logger = LoggerFactory.getLogger(GetInfoOnPDF.class);
static ObjectMapper objectMapper = new ObjectMapper(); static ObjectMapper objectMapper = new ObjectMapper();
@PostMapping(consumes = "multipart/form-data", value = "/get-info-on-pdf") @PostMapping(consumes = "multipart/form-data", value = "/get-info-on-pdf")
@@ -224,7 +222,7 @@ public class GetInfoOnPDF {
javascriptArray.add(jsNode); javascriptArray.add(jsNode);
} }
} catch (IOException e) { } catch (IOException e) {
logger.error("exception", e); log.error("exception", e);
} }
} }
} }
@@ -257,7 +255,7 @@ public class GetInfoOnPDF {
} }
} catch (Exception e) { } catch (Exception e) {
// TODO Auto-generated catch block // TODO Auto-generated catch block
logger.error("exception", e); log.error("exception", e);
} }
boolean isPdfACompliant = checkForStandard(pdfBoxDoc, "PDF/A"); boolean isPdfACompliant = checkForStandard(pdfBoxDoc, "PDF/A");
@@ -309,7 +307,7 @@ public class GetInfoOnPDF {
new XmpSerializer().serialize(xmpMeta, os, true); new XmpSerializer().serialize(xmpMeta, os, true);
xmpString = new String(os.toByteArray(), StandardCharsets.UTF_8); xmpString = new String(os.toByteArray(), StandardCharsets.UTF_8);
} catch (XmpParsingException | IOException e) { } catch (XmpParsingException | IOException e) {
logger.error("exception", e); log.error("exception", e);
} }
} }
@@ -322,27 +320,14 @@ public class GetInfoOnPDF {
PDEncryption pdfEncryption = pdfBoxDoc.getEncryption(); PDEncryption pdfEncryption = pdfBoxDoc.getEncryption();
encryption.put("EncryptionAlgorithm", pdfEncryption.getFilter()); encryption.put("EncryptionAlgorithm", pdfEncryption.getFilter());
encryption.put("KeyLength", pdfEncryption.getLength()); encryption.put("KeyLength", pdfEncryption.getLength());
AccessPermission ap = pdfBoxDoc.getCurrentAccessPermission();
if (ap != null) {
ObjectNode permissionsNode = objectMapper.createObjectNode();
permissionsNode.put("CanAssembleDocument", ap.canAssembleDocument());
permissionsNode.put("CanExtractContent", ap.canExtractContent());
permissionsNode.put(
"CanExtractForAccessibility", ap.canExtractForAccessibility());
permissionsNode.put("CanFillInForm", ap.canFillInForm());
permissionsNode.put("CanModify", ap.canModify());
permissionsNode.put("CanModifyAnnotations", ap.canModifyAnnotations());
permissionsNode.put("CanPrint", ap.canPrint());
encryption.set(
"Permissions", permissionsNode); // set the node under "Permissions"
}
// Add other encryption-related properties as needed // Add other encryption-related properties as needed
} else { } else {
encryption.put("IsEncrypted", false); encryption.put("IsEncrypted", false);
} }
ObjectNode permissionsNode = objectMapper.createObjectNode();
setNodePermissions(pdfBoxDoc, permissionsNode);
ObjectNode pageInfoParent = objectMapper.createObjectNode(); ObjectNode pageInfoParent = objectMapper.createObjectNode();
for (int pageNum = 0; pageNum < pdfBoxDoc.getNumberOfPages(); pageNum++) { for (int pageNum = 0; pageNum < pdfBoxDoc.getNumberOfPages(); pageNum++) {
ObjectNode pageInfo = objectMapper.createObjectNode(); ObjectNode pageInfo = objectMapper.createObjectNode();
@@ -584,6 +569,7 @@ public class GetInfoOnPDF {
jsonOutput.set("DocumentInfo", docInfoNode); jsonOutput.set("DocumentInfo", docInfoNode);
jsonOutput.set("Compliancy", compliancy); jsonOutput.set("Compliancy", compliancy);
jsonOutput.set("Encryption", encryption); jsonOutput.set("Encryption", encryption);
jsonOutput.set("Permissions", permissionsNode); // set the node under "Permissions"
jsonOutput.set("Other", other); jsonOutput.set("Other", other);
jsonOutput.set("PerPageInfo", pageInfoParent); jsonOutput.set("PerPageInfo", pageInfoParent);
@@ -597,11 +583,29 @@ public class GetInfoOnPDF {
MediaType.APPLICATION_JSON); MediaType.APPLICATION_JSON);
} catch (Exception e) { } catch (Exception e) {
logger.error("exception", e); log.error("exception", e);
} }
return null; return null;
} }
private void setNodePermissions(PDDocument pdfBoxDoc, ObjectNode permissionsNode) {
AccessPermission ap = pdfBoxDoc.getCurrentAccessPermission();
permissionsNode.put("Document Assembly", getPermissionState(ap.canAssembleDocument()));
permissionsNode.put("Extracting Content", getPermissionState(ap.canExtractContent()));
permissionsNode.put(
"Extracting for accessibility",
getPermissionState(ap.canExtractForAccessibility()));
permissionsNode.put("Form Filling", getPermissionState(ap.canFillInForm()));
permissionsNode.put("Modifying", getPermissionState(ap.canModify()));
permissionsNode.put("Modifying annotations", getPermissionState(ap.canModifyAnnotations()));
permissionsNode.put("Printing", getPermissionState(ap.canPrint()));
}
private String getPermissionState(boolean state) {
return state ? "Allowed" : "Not Allowed";
}
private static void addOutlinesToArray(PDOutlineItem outline, ArrayNode arrayNode) { private static void addOutlinesToArray(PDOutlineItem outline, ArrayNode arrayNode) {
if (outline == null) return; if (outline == null) return;
@@ -695,7 +699,7 @@ public class GetInfoOnPDF {
Exception Exception
e) { // Catching general exception for brevity, ideally you'd catch specific e) { // Catching general exception for brevity, ideally you'd catch specific
// exceptions. // exceptions.
logger.error("exception", e); log.error("exception", e);
} }
return false; return false;

View File

@@ -5,8 +5,6 @@ import java.io.IOException;
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.encryption.AccessPermission; import org.apache.pdfbox.pdmodel.encryption.AccessPermission;
import org.apache.pdfbox.pdmodel.encryption.StandardProtectionPolicy; import org.apache.pdfbox.pdmodel.encryption.StandardProtectionPolicy;
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.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.ModelAttribute;
@@ -29,8 +27,6 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@Tag(name = "Security", description = "Security APIs") @Tag(name = "Security", description = "Security APIs")
public class PasswordController { public class PasswordController {
private static final Logger logger = LoggerFactory.getLogger(PasswordController.class);
private final CustomPDDocumentFactory pdfDocumentFactory; private final CustomPDDocumentFactory pdfDocumentFactory;
@Autowired @Autowired
@@ -42,7 +38,8 @@ public class PasswordController {
@Operation( @Operation(
summary = "Remove password from a PDF file", summary = "Remove password from a PDF file",
description = description =
"This endpoint removes the password from a protected PDF file. Users need to provide the existing password. Input:PDF Output:PDF Type:SISO") "This endpoint removes the password from a protected PDF file. Users need to provide the"
+ " existing password. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> removePassword(@ModelAttribute PDFPasswordRequest request) public ResponseEntity<byte[]> removePassword(@ModelAttribute PDFPasswordRequest request)
throws IOException { throws IOException {
MultipartFile fileInput = request.getFileInput(); MultipartFile fileInput = request.getFileInput();
@@ -60,7 +57,8 @@ public class PasswordController {
@Operation( @Operation(
summary = "Add password to a PDF file", summary = "Add password to a PDF file",
description = description =
"This endpoint adds password protection to a PDF file. Users can specify a set of permissions that should be applied to the file. Input:PDF Output:PDF") "This endpoint adds password protection to a PDF file. Users can specify a set of"
+ " permissions that should be applied to the file. Input:PDF Output:PDF")
public ResponseEntity<byte[]> addPassword(@ModelAttribute AddPasswordRequest request) public ResponseEntity<byte[]> addPassword(@ModelAttribute AddPasswordRequest request)
throws IOException { throws IOException {
MultipartFile fileInput = request.getFileInput(); MultipartFile fileInput = request.getFileInput();

View File

@@ -8,8 +8,6 @@ import java.util.List;
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPageContentStream; import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.common.PDRectangle; import org.apache.pdfbox.pdmodel.common.PDRectangle;
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.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.ModelAttribute;
@@ -22,6 +20,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 lombok.extern.slf4j.Slf4j;
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;
@@ -31,11 +30,10 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@RequestMapping("/api/v1/security") @RequestMapping("/api/v1/security")
@Slf4j
@Tag(name = "Security", description = "Security APIs") @Tag(name = "Security", description = "Security APIs")
public class RedactController { public class RedactController {
private static final Logger logger = LoggerFactory.getLogger(RedactController.class);
private final CustomPDDocumentFactory pdfDocumentFactory; private final CustomPDDocumentFactory pdfDocumentFactory;
@Autowired @Autowired
@@ -47,7 +45,8 @@ public class RedactController {
@Operation( @Operation(
summary = "Redacts listOfText in a PDF document", summary = "Redacts listOfText in a PDF document",
description = description =
"This operation takes an input PDF file and redacts the provided listOfText. Input:PDF, Output:PDF, Type:SISO") "This operation takes an input PDF file and redacts the provided listOfText. Input:PDF,"
+ " Output:PDF, Type:SISO")
public ResponseEntity<byte[]> redactPdf(@ModelAttribute RedactPdfRequest request) public ResponseEntity<byte[]> redactPdf(@ModelAttribute RedactPdfRequest request)
throws Exception { throws Exception {
MultipartFile file = request.getFileInput(); MultipartFile file = request.getFileInput();
@@ -68,7 +67,7 @@ public class RedactController {
} }
redactColor = Color.decode(colorString); redactColor = Color.decode(colorString);
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
logger.warn("Invalid color string provided. Using default color BLACK for redaction."); log.warn("Invalid color string provided. Using default color BLACK for redaction.");
redactColor = Color.BLACK; redactColor = Color.BLACK;
} }

View File

@@ -8,8 +8,6 @@ import org.apache.pdfbox.pdmodel.PDDocumentCatalog;
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm; import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
import org.apache.pdfbox.pdmodel.interactive.form.PDField; import org.apache.pdfbox.pdmodel.interactive.form.PDField;
import org.apache.pdfbox.pdmodel.interactive.form.PDSignatureField; import org.apache.pdfbox.pdmodel.interactive.form.PDSignatureField;
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.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.ModelAttribute;
@@ -31,8 +29,6 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@Tag(name = "Security", description = "Security APIs") @Tag(name = "Security", description = "Security APIs")
public class RemoveCertSignController { public class RemoveCertSignController {
private static final Logger logger = LoggerFactory.getLogger(RemoveCertSignController.class);
private final CustomPDDocumentFactory pdfDocumentFactory; private final CustomPDDocumentFactory pdfDocumentFactory;
@Autowired @Autowired
@@ -44,7 +40,8 @@ public class RemoveCertSignController {
@Operation( @Operation(
summary = "Remove digital signature from PDF", summary = "Remove digital signature from PDF",
description = description =
"This endpoint accepts a PDF file and returns the PDF file without the digital signature. Input: PDF, Output: PDF") "This endpoint accepts a PDF file and returns the PDF file without the digital signature."
+ " Input: PDF, Output: PDF")
public ResponseEntity<byte[]> removeCertSignPDF(@ModelAttribute PDFFile request) public ResponseEntity<byte[]> removeCertSignPDF(@ModelAttribute PDFFile request)
throws Exception { throws Exception {
MultipartFile pdf = request.getFileInput(); MultipartFile pdf = request.getFileInput();

View File

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

View File

@@ -69,6 +69,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();
String customColor = request.getCustomColor();
boolean convertPdfToImage = request.isConvertPDFToImage(); boolean convertPdfToImage = request.isConvertPDFToImage();
// Load the input PDF // Load the input PDF
@@ -97,7 +98,8 @@ public class WatermarkController {
widthSpacer, widthSpacer,
heightSpacer, heightSpacer,
fontSize, fontSize,
alphabet); alphabet,
customColor);
} else if ("image".equalsIgnoreCase(watermarkType)) { } else if ("image".equalsIgnoreCase(watermarkType)) {
addImageWatermark( addImageWatermark(
contentStream, contentStream,
@@ -136,7 +138,8 @@ public class WatermarkController {
int widthSpacer, int widthSpacer,
int heightSpacer, int heightSpacer,
float fontSize, float fontSize,
String alphabet) String alphabet,
String colorString)
throws IOException { throws IOException {
String resourceDir = ""; String resourceDir = "";
PDFont font = new PDType1Font(Standard14Fonts.FontName.HELVETICA); PDFont font = new PDType1Font(Standard14Fonts.FontName.HELVETICA);
@@ -173,7 +176,18 @@ public class WatermarkController {
} }
contentStream.setFont(font, fontSize); contentStream.setFont(font, fontSize);
contentStream.setNonStrokingColor(Color.LIGHT_GRAY);
Color redactColor;
try {
if (!colorString.startsWith("#")) {
colorString = "#" + colorString;
}
redactColor = Color.decode(colorString);
} catch (NumberFormatException e) {
redactColor = Color.LIGHT_GRAY;
}
contentStream.setNonStrokingColor(redactColor);
String[] textLines = watermarkText.split("\\\\n"); String[] textLines = watermarkText.split("\\\\n");
float maxLineWidth = 0; float maxLineWidth = 0;

View File

@@ -15,8 +15,6 @@ import java.util.Objects;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
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.core.io.Resource; import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.ResourceLoader;
@@ -31,16 +29,16 @@ import com.fasterxml.jackson.databind.ObjectMapper;
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 lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.controller.api.pipeline.UserServiceInterface; import stirling.software.SPDF.controller.api.pipeline.UserServiceInterface;
import stirling.software.SPDF.model.SignatureFile; import stirling.software.SPDF.model.SignatureFile;
import stirling.software.SPDF.service.SignatureService; import stirling.software.SPDF.service.SignatureService;
@Controller @Controller
@Tag(name = "General", description = "General APIs") @Tag(name = "General", description = "General APIs")
@Slf4j
public class GeneralWebController { public class GeneralWebController {
private static final Logger logger = LoggerFactory.getLogger(GeneralWebController.class);
@GetMapping("/pipeline") @GetMapping("/pipeline")
@Hidden @Hidden
public String pipelineForm(Model model) { public String pipelineForm(Model model) {
@@ -82,7 +80,7 @@ public class GeneralWebController {
} }
} catch (IOException e) { } catch (IOException e) {
logger.error("exception", e); log.error("exception", e);
} }
} }
if (pipelineConfigsWithNames.size() == 0) { if (pipelineConfigsWithNames.size() == 0) {

View File

@@ -6,8 +6,6 @@ import java.nio.charset.StandardCharsets;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
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.core.io.ClassPathResource; import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
@@ -22,14 +20,14 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Hidden;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.Dependency; import stirling.software.SPDF.model.Dependency;
@Controller @Controller
@Slf4j
public class HomeWebController { public class HomeWebController {
private static final Logger logger = LoggerFactory.getLogger(HomeWebController.class);
@GetMapping("/about") @GetMapping("/about")
@Hidden @Hidden
public String gameForm(Model model) { public String gameForm(Model model) {
@@ -50,7 +48,7 @@ public class HomeWebController {
mapper.readValue(json, new TypeReference<Map<String, List<Dependency>>>() {}); mapper.readValue(json, new TypeReference<Map<String, List<Dependency>>>() {});
model.addAttribute("dependencies", data.get("dependencies")); model.addAttribute("dependencies", data.get("dependencies"));
} catch (IOException e) { } catch (IOException e) {
logger.error("exception", e); log.error("exception", e);
} }
return "licenses"; return "licenses";
} }

View File

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

View File

@@ -73,6 +73,7 @@ public class ApplicationProperties {
private int loginAttemptCount; private int loginAttemptCount;
private long loginResetTimeMinutes; private long loginResetTimeMinutes;
private String loginMethod = "all"; private String loginMethod = "all";
private String customGlobalAPIKey;
public Boolean isAltLogin() { public Boolean isAltLogin() {
return saml2.getEnabled() || oauth2.getEnabled(); return saml2.getEnabled() || oauth2.getEnabled();
@@ -121,18 +122,19 @@ public class ApplicationProperties {
@Getter @Getter
@Setter @Setter
@ToString
public static class SAML2 { public static class SAML2 {
private Boolean enabled = false; private Boolean enabled = false;
private Boolean autoCreateUser = false; private Boolean autoCreateUser = false;
private Boolean blockRegistration = false; private Boolean blockRegistration = false;
private String registrationId = "stirling"; private String registrationId = "stirling";
private String idpMetadataUri; @ToString.Exclude private String idpMetadataUri;
private String idpSingleLogoutUrl; private String idpSingleLogoutUrl;
private String idpSingleLoginUrl; private String idpSingleLoginUrl;
private String idpIssuer; private String idpIssuer;
private String idpCert; private String idpCert;
private String privateKey; @ToString.Exclude private String privateKey;
private String spCert; @ToString.Exclude private String spCert;
public InputStream getIdpMetadataUri() throws IOException { public InputStream getIdpMetadataUri() throws IOException {
if (idpMetadataUri.startsWith("classpath:")) { if (idpMetadataUri.startsWith("classpath:")) {
@@ -285,6 +287,7 @@ public class ApplicationProperties {
public static class AutomaticallyGenerated { public static class AutomaticallyGenerated {
@ToString.Exclude private String key; @ToString.Exclude private String key;
private String UUID; private String UUID;
private String appVersion;
} }
@Data @Data

View File

@@ -5,8 +5,6 @@ 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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
@@ -14,18 +12,20 @@ import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.utils.GeneralUtils; import stirling.software.SPDF.utils.GeneralUtils;
@Data @Data
@NoArgsConstructor @NoArgsConstructor
@Slf4j
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
public class PDFWithPageNums extends PDFFile { public class PDFWithPageNums extends PDFFile {
private static final Logger logger = LoggerFactory.getLogger(PDFWithPageNums.class);
@Schema( @Schema(
description = description =
"The pages to select, Supports ranges (e.g., '1,3,5-9'), or 'all' or functions in the format 'an+b' where 'a' is the multiplier of the page number 'n', and 'b' is a constant (e.g., '2n+1', '3n', '6n-5')\"") "The pages to select, Supports ranges (e.g., '1,3,5-9'), or 'all' or functions in the"
+ " format 'an+b' where 'a' is the multiplier of the page number 'n', and 'b' is a"
+ " constant (e.g., '2n+1', '3n', '6n-5')\"")
private String pageNumbers; private String pageNumbers;
@Hidden @Hidden
@@ -35,7 +35,7 @@ public class PDFWithPageNums extends PDFFile {
pageCount = Loader.loadPDF(getFileInput().getBytes()).getNumberOfPages(); pageCount = Loader.loadPDF(getFileInput().getBytes()).getNumberOfPages();
} catch (IOException e) { } catch (IOException e) {
// TODO Auto-generated catch block // TODO Auto-generated catch block
logger.error("exception", e); log.error("exception", e);
} }
return GeneralUtils.parsePageList(pageNumbers, pageCount, zeroCount); return GeneralUtils.parsePageList(pageNumbers, pageCount, zeroCount);
} }

View File

@@ -45,6 +45,9 @@ 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 = "The color for watermark", defaultValue = "#d3d3d3")
private String customColor = "#d3d3d3";
@Schema(description = "Convert the redacted PDF to an image", defaultValue = "false") @Schema(description = "Convert the redacted PDF to an image", defaultValue = "false")
private boolean convertPDFToImage; private boolean convertPDFToImage;
} }

View File

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

View File

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

View File

@@ -0,0 +1,158 @@
package stirling.software.SPDF.service;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.cert.CertPath;
import java.security.cert.CertPathValidator;
import java.security.cert.CertificateExpiredException;
import java.security.cert.CertificateFactory;
import java.security.cert.CertificateNotYetValidException;
import java.security.cert.PKIXParameters;
import java.security.cert.TrustAnchor;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.springframework.stereotype.Service;
import io.github.pixee.security.BoundedLineReader;
import jakarta.annotation.PostConstruct;
@Service
public class CertificateValidationService {
private KeyStore trustStore;
@PostConstruct
private void initializeTrustStore() throws Exception {
trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
trustStore.load(null, null);
loadMozillaCertificates();
}
private void loadMozillaCertificates() throws Exception {
try (InputStream is = getClass().getResourceAsStream("/certdata.txt")) {
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
String line;
StringBuilder certData = new StringBuilder();
boolean inCert = false;
int certCount = 0;
while ((line = BoundedLineReader.readLine(reader, 5_000_000)) != null) {
if (line.startsWith("CKA_VALUE MULTILINE_OCTAL")) {
inCert = true;
certData = new StringBuilder();
continue;
}
if (inCert) {
if ("END".equals(line)) {
inCert = false;
byte[] certBytes = parseOctalData(certData.toString());
if (certBytes != null) {
CertificateFactory cf = CertificateFactory.getInstance("X.509");
X509Certificate cert =
(X509Certificate)
cf.generateCertificate(
new ByteArrayInputStream(certBytes));
trustStore.setCertificateEntry("mozilla-cert-" + certCount++, cert);
}
} else {
certData.append(line).append("\n");
}
}
}
}
}
private byte[] parseOctalData(String data) {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
String[] tokens = data.split("\\\\");
for (String token : tokens) {
token = token.trim();
if (!token.isEmpty()) {
baos.write(Integer.parseInt(token, 8));
}
}
return baos.toByteArray();
} catch (Exception e) {
return null;
}
}
public boolean validateCertificateChain(X509Certificate cert) {
try {
CertPathValidator validator = CertPathValidator.getInstance("PKIX");
CertificateFactory cf = CertificateFactory.getInstance("X.509");
List<X509Certificate> certList = Arrays.asList(cert);
CertPath certPath = cf.generateCertPath(certList);
Set<TrustAnchor> anchors = new HashSet<>();
Enumeration<String> aliases = trustStore.aliases();
while (aliases.hasMoreElements()) {
Object trustCert = trustStore.getCertificate(aliases.nextElement());
if (trustCert instanceof X509Certificate) {
anchors.add(new TrustAnchor((X509Certificate) trustCert, null));
}
}
PKIXParameters params = new PKIXParameters(anchors);
params.setRevocationEnabled(false);
validator.validate(certPath, params);
return true;
} catch (Exception e) {
return false;
}
}
public boolean validateTrustStore(X509Certificate cert) {
try {
Enumeration<String> aliases = trustStore.aliases();
while (aliases.hasMoreElements()) {
Object trustCert = trustStore.getCertificate(aliases.nextElement());
if (trustCert instanceof X509Certificate && cert.equals(trustCert)) {
return true;
}
}
return false;
} catch (KeyStoreException e) {
return false;
}
}
public boolean isRevoked(X509Certificate cert) {
try {
cert.checkValidity();
return false;
} catch (CertificateExpiredException | CertificateNotYetValidException e) {
return true;
}
}
public boolean validateCertificateChainWithCustomCert(
X509Certificate cert, X509Certificate customCert) {
try {
cert.verify(customCert.getPublicKey());
return true;
} catch (Exception e) {
return false;
}
}
public boolean validateTrustWithCustomCert(X509Certificate cert, X509Certificate customCert) {
try {
// Compare the issuer of the signature certificate with the custom certificate
return cert.getIssuerX500Principal().equals(customCert.getSubjectX500Principal());
} catch (Exception e) {
return false;
}
}
}

View File

@@ -7,20 +7,18 @@ import java.io.InputStream;
import org.apache.pdfbox.Loader; import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocument;
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.Component; import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.PdfMetadata; import stirling.software.SPDF.model.PdfMetadata;
import stirling.software.SPDF.model.api.PDFFile; import stirling.software.SPDF.model.api.PDFFile;
@Component @Component
@Slf4j
public class CustomPDDocumentFactory { public class CustomPDDocumentFactory {
private static final Logger logger = LoggerFactory.getLogger(CustomPDDocumentFactory.class);
private final PdfMetadataService pdfMetadataService; private final PdfMetadataService pdfMetadataService;
@Autowired @Autowired
@@ -133,10 +131,10 @@ public class CustomPDDocumentFactory {
private PDDocument removezeropassword(PDDocument document) throws IOException { private PDDocument removezeropassword(PDDocument document) throws IOException {
if (document.isEncrypted()) { if (document.isEncrypted()) {
try { try {
logger.info("Removing security from the source document"); log.info("Removing security from the source document");
document.setAllSecurityToBeRemoved(true); document.setAllSecurityToBeRemoved(true);
} catch (Exception e) { } catch (Exception e) {
logger.warn("Cannot decrypt the pdf"); log.warn("Cannot decrypt the pdf");
} }
} }
return document; return document;

View File

@@ -24,7 +24,7 @@ public class MetricsAggregatorService {
this.postHogService = postHogService; this.postHogService = postHogService;
} }
@Scheduled(fixedRate = 1800000) // Run every 30 minutes @Scheduled(fixedRate = 7200000) // Run every 2 hours
public void aggregateAndSendMetrics() { public void aggregateAndSendMetrics() {
Map<String, Object> metrics = new HashMap<>(); Map<String, Object> metrics = new HashMap<>();
Search.in(meterRegistry) Search.in(meterRegistry)

View File

@@ -31,11 +31,13 @@ public class PostHogService {
private final ApplicationProperties applicationProperties; private final ApplicationProperties applicationProperties;
private final UserServiceInterface userService; private final UserServiceInterface userService;
private final Environment env; private final Environment env;
private boolean configDirMounted;
@Autowired @Autowired
public PostHogService( public PostHogService(
PostHog postHog, PostHog postHog,
@Qualifier("UUID") String uuid, @Qualifier("UUID") String uuid,
@Qualifier("configDirMounted") boolean configDirMounted,
@Qualifier("appVersion") String appVersion, @Qualifier("appVersion") String appVersion,
ApplicationProperties applicationProperties, ApplicationProperties applicationProperties,
@Autowired(required = false) UserServiceInterface userService, @Autowired(required = false) UserServiceInterface userService,
@@ -46,6 +48,7 @@ public class PostHogService {
this.applicationProperties = applicationProperties; this.applicationProperties = applicationProperties;
this.userService = userService; this.userService = userService;
this.env = env; this.env = env;
this.configDirMounted = configDirMounted;
captureSystemInfo(); captureSystemInfo();
} }
@@ -80,6 +83,7 @@ public class PostHogService {
deploymentType = "DOCKER"; deploymentType = "DOCKER";
} }
metrics.put("deployment_type", deploymentType); metrics.put("deployment_type", deploymentType);
metrics.put("mounted_config_dir", configDirMounted);
// System info // System info
metrics.put("os_name", System.getProperty("os.name")); metrics.put("os_name", System.getProperty("os.name"));

View File

@@ -9,16 +9,17 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.stream.Stream; import java.util.stream.Stream;
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.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import lombok.extern.slf4j.Slf4j;
@Component @Component
@Slf4j
public class FileMonitor { public class FileMonitor {
private static final Logger logger = LoggerFactory.getLogger(FileMonitor.class);
private final Map<Path, WatchKey> path2KeyMapping; private final Map<Path, WatchKey> path2KeyMapping;
private final Set<Path> newlyDiscoveredFiles; private final Set<Path> newlyDiscoveredFiles;
private final ConcurrentHashMap.KeySetView<Path, Boolean> readyForProcessingFiles; private final ConcurrentHashMap.KeySetView<Path, Boolean> readyForProcessingFiles;
@@ -53,7 +54,7 @@ public class FileMonitor {
private void recursivelyRegisterEntry(Path dir) throws IOException { private void recursivelyRegisterEntry(Path dir) throws IOException {
WatchKey key = dir.register(watchService, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY); WatchKey key = dir.register(watchService, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY);
path2KeyMapping.put(dir, key); path2KeyMapping.put(dir, key);
logger.info("Registered directory: {}", dir); log.info("Registered directory: {}", dir);
try (Stream<Path> directoryVisitor = Files.walk(dir, 1)) { try (Stream<Path> directoryVisitor = Files.walk(dir, 1)) {
final Iterator<Path> iterator = directoryVisitor.iterator(); final Iterator<Path> iterator = directoryVisitor.iterator();
@@ -80,14 +81,13 @@ public class FileMonitor {
readyForProcessingFiles.clear(); readyForProcessingFiles.clear();
if (path2KeyMapping.isEmpty()) { if (path2KeyMapping.isEmpty()) {
logger.warn( log.warn("not monitoring any directory, even the root directory itself: {}", rootDir);
"not monitoring any directory, even the root directory itself: {}", rootDir);
if (Files.exists( if (Files.exists(
rootDir)) { // if the root directory exists, re-register the root directory rootDir)) { // if the root directory exists, re-register the root directory
try { try {
recursivelyRegisterEntry(rootDir); recursivelyRegisterEntry(rootDir);
} catch (IOException e) { } catch (IOException e) {
logger.error("unable to register monitoring", e); log.error("unable to register monitoring", e);
} }
} }
} }
@@ -122,7 +122,7 @@ public class FileMonitor {
handleFileModification(relativePathFromRoot); handleFileModification(relativePathFromRoot);
} }
} catch (Exception e) { } catch (Exception e) {
logger.error("Error while processing file: {}", path, e); log.error("Error while processing file: {}", path, e);
} }
}); });

View File

@@ -27,8 +27,6 @@ import org.simpleyaml.configuration.file.YamlFile;
import org.simpleyaml.configuration.file.YamlFileWrapper; import org.simpleyaml.configuration.file.YamlFileWrapper;
import org.simpleyaml.configuration.implementation.SimpleYamlImplementation; import org.simpleyaml.configuration.implementation.SimpleYamlImplementation;
import org.simpleyaml.configuration.implementation.snakeyaml.lib.DumperOptions; import org.simpleyaml.configuration.implementation.snakeyaml.lib.DumperOptions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import com.fathzer.soft.javaluator.DoubleEvaluator; import com.fathzer.soft.javaluator.DoubleEvaluator;
@@ -36,9 +34,10 @@ import com.fathzer.soft.javaluator.DoubleEvaluator;
import io.github.pixee.security.HostValidator; import io.github.pixee.security.HostValidator;
import io.github.pixee.security.Urls; import io.github.pixee.security.Urls;
public class GeneralUtils { import lombok.extern.slf4j.Slf4j;
private static final Logger logger = LoggerFactory.getLogger(GeneralUtils.class); @Slf4j
public class GeneralUtils {
public static File convertMultipartFileToFile(MultipartFile multipartFile) throws IOException { public static File convertMultipartFileToFile(MultipartFile multipartFile) throws IOException {
File tempFile = Files.createTempFile("temp", null).toFile(); File tempFile = Files.createTempFile("temp", null).toFile();
@@ -88,15 +87,50 @@ public class GeneralUtils {
public static boolean isURLReachable(String urlStr) { public static boolean isURLReachable(String urlStr) {
try { try {
// Parse the URL
URL url = URI.create(urlStr).toURL(); URL url = URI.create(urlStr).toURL();
// Allow only http and https protocols
String protocol = url.getProtocol();
if (!protocol.equals("http") && !protocol.equals("https")) {
return false; // Disallow other protocols
}
// Check if the host is a local address
String host = url.getHost();
if (isLocalAddress(host)) {
return false; // Exclude local addresses
}
// Check if the URL is reachable
HttpURLConnection connection = (HttpURLConnection) url.openConnection(); HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("HEAD"); connection.setRequestMethod("HEAD");
// connection.setConnectTimeout(5000); // Set connection timeout
// connection.setReadTimeout(5000); // Set read timeout
int responseCode = connection.getResponseCode(); int responseCode = connection.getResponseCode();
return (200 <= responseCode && responseCode <= 399); return (200 <= responseCode && responseCode <= 399);
} catch (MalformedURLException e) { } catch (Exception e) {
return false; return false; // Return false in case of any exception
} catch (IOException e) { }
return false; }
private static boolean isLocalAddress(String host) {
try {
// Resolve DNS to IP address
InetAddress address = InetAddress.getByName(host);
// Check for local addresses
return address.isAnyLocalAddress()
|| // Matches 0.0.0.0 or similar
address.isLoopbackAddress()
|| // Matches 127.0.0.1 or ::1
address.isSiteLocalAddress()
|| // Matches private IPv4 ranges: 192.168.x.x, 10.x.x.x, 172.16.x.x to
// 172.31.x.x
address.getHostAddress()
.startsWith("fe80:"); // Matches link-local IPv6 addresses
} catch (Exception e) {
return false; // Return false for invalid or unresolved addresses
} }
} }
@@ -266,7 +300,7 @@ public class GeneralUtils {
try { try {
Files.createDirectories(folder); Files.createDirectories(folder);
} catch (IOException e) { } catch (IOException e) {
logger.error("exception", e); log.error("exception", e);
return false; return false;
} }
} }
@@ -289,6 +323,10 @@ public class GeneralUtils {
saveKeyToConfig(id, key, true); saveKeyToConfig(id, key, true);
} }
public static void saveKeyToConfig(String id, boolean key) throws IOException {
saveKeyToConfig(id, key, true);
}
public static void saveKeyToConfig(String id, String key, boolean autoGenerated) public static void saveKeyToConfig(String id, String key, boolean autoGenerated)
throws IOException { throws IOException {
Path path = Paths.get("configs", "settings.yml"); // Target the configs/settings.yml Path path = Paths.get("configs", "settings.yml"); // Target the configs/settings.yml
@@ -307,6 +345,24 @@ public class GeneralUtils {
settingsYml.save(); settingsYml.save();
} }
public static void saveKeyToConfig(String id, boolean key, boolean autoGenerated)
throws IOException {
Path path = Paths.get("configs", "settings.yml");
final YamlFile settingsYml = new YamlFile(path.toFile());
DumperOptions yamlOptionssettingsYml =
((SimpleYamlImplementation) settingsYml.getImplementation()).getDumperOptions();
yamlOptionssettingsYml.setSplitLines(false);
settingsYml.loadWithComments();
YamlFileWrapper writer = settingsYml.path(id).set(key);
if (autoGenerated) {
writer.comment("# Automatically Generated Settings (Do Not Edit Directly)");
}
settingsYml.save();
}
public static String generateMachineFingerprint() { public static String generateMachineFingerprint() {
try { try {
// Get the MAC address // Get the MAC address
@@ -349,4 +405,33 @@ public class GeneralUtils {
return "GenericID"; return "GenericID";
} }
} }
public static boolean isVersionHigher(String currentVersion, String compareVersion) {
if (currentVersion == null || compareVersion == null) {
return false;
}
// Split versions into components
String[] current = currentVersion.split("\\.");
String[] compare = compareVersion.split("\\.");
// Get the length of the shorter version array
int length = Math.min(current.length, compare.length);
// Compare each component
for (int i = 0; i < length; i++) {
int currentPart = Integer.parseInt(current[i]);
int comparePart = Integer.parseInt(compare[i]);
if (currentPart > comparePart) {
return true;
}
if (currentPart < comparePart) {
return false;
}
}
// If all components so far are equal, the longer version is considered higher
return current.length > compare.length;
}
} }

View File

@@ -12,8 +12,6 @@ import java.nio.ByteBuffer;
import javax.imageio.ImageIO; import javax.imageio.ImageIO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import com.drew.imaging.ImageMetadataReader; import com.drew.imaging.ImageMetadataReader;
@@ -22,9 +20,10 @@ import com.drew.metadata.Metadata;
import com.drew.metadata.MetadataException; import com.drew.metadata.MetadataException;
import com.drew.metadata.exif.ExifSubIFDDirectory; import com.drew.metadata.exif.ExifSubIFDDirectory;
public class ImageProcessingUtils { import lombok.extern.slf4j.Slf4j;
private static final Logger logger = LoggerFactory.getLogger(PdfUtils.class); @Slf4j
public class ImageProcessingUtils {
static BufferedImage convertColorType(BufferedImage sourceImage, String colorType) { static BufferedImage convertColorType(BufferedImage sourceImage, String colorType) {
BufferedImage convertedImage; BufferedImage convertedImage;
@@ -97,7 +96,7 @@ public class ImageProcessingUtils {
case 8: case 8:
return 270; return 270;
default: default:
logger.warn("Unknown orientation tag: {}", orientationTag); log.warn("Unknown orientation tag: {}", orientationTag);
return 0; return 0;
} }
} catch (ImageProcessingException | MetadataException e) { } catch (ImageProcessingException | MetadataException e) {

View File

@@ -14,8 +14,6 @@ import java.util.zip.ZipOutputStream;
import org.apache.commons.io.FileUtils; import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
@@ -23,10 +21,11 @@ import org.springframework.web.multipart.MultipartFile;
import io.github.pixee.security.Filenames; import io.github.pixee.security.Filenames;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult; import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
@Slf4j
public class PDFToFile { public class PDFToFile {
private static final Logger logger = LoggerFactory.getLogger(PDFToFile.class);
public ResponseEntity<byte[]> processPdfToHtml(MultipartFile inputFile) public ResponseEntity<byte[]> processPdfToHtml(MultipartFile inputFile)
throws IOException, InterruptedException { throws IOException, InterruptedException {
@@ -77,12 +76,12 @@ public class PDFToFile {
try (FileInputStream fis = new FileInputStream(outputFile)) { try (FileInputStream fis = new FileInputStream(outputFile)) {
IOUtils.copy(fis, zipOutputStream); IOUtils.copy(fis, zipOutputStream);
} catch (IOException e) { } catch (IOException e) {
logger.error("Exception writing zip entry", e); log.error("Exception writing zip entry", e);
} }
zipOutputStream.closeEntry(); zipOutputStream.closeEntry();
} }
} catch (IOException e) { } catch (IOException e) {
logger.error("Exception writing zip", e); log.error("Exception writing zip", e);
} }
fileBytes = byteArrayOutputStream.toByteArray(); fileBytes = byteArrayOutputStream.toByteArray();
@@ -174,13 +173,13 @@ public class PDFToFile {
try (FileInputStream fis = new FileInputStream(outputFile)) { try (FileInputStream fis = new FileInputStream(outputFile)) {
IOUtils.copy(fis, zipOutputStream); IOUtils.copy(fis, zipOutputStream);
} catch (IOException e) { } catch (IOException e) {
logger.error("Exception writing zip entry", e); log.error("Exception writing zip entry", e);
} }
zipOutputStream.closeEntry(); zipOutputStream.closeEntry();
} }
} catch (IOException e) { } catch (IOException e) {
logger.error("Exception writing zip", e); log.error("Exception writing zip", e);
} }
fileBytes = byteArrayOutputStream.toByteArray(); fileBytes = byteArrayOutputStream.toByteArray();

View File

@@ -30,18 +30,16 @@ import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
import org.apache.pdfbox.rendering.ImageType; import org.apache.pdfbox.rendering.ImageType;
import org.apache.pdfbox.rendering.PDFRenderer; import org.apache.pdfbox.rendering.PDFRenderer;
import org.apache.pdfbox.text.PDFTextStripper; import org.apache.pdfbox.text.PDFTextStripper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import io.github.pixee.security.Filenames; import io.github.pixee.security.Filenames;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.service.CustomPDDocumentFactory; import stirling.software.SPDF.service.CustomPDDocumentFactory;
@Slf4j
public class PdfUtils { public class PdfUtils {
private static final Logger logger = LoggerFactory.getLogger(PdfUtils.class);
public static PDRectangle textToPageSize(String size) { public static PDRectangle textToPageSize(String size) {
switch (size.toUpperCase()) { switch (size.toUpperCase()) {
case "A0": case "A0":
@@ -310,7 +308,7 @@ public class PdfUtils {
} }
// Log that the image was successfully written to the byte array // Log that the image was successfully written to the byte array
logger.info("Image successfully written to byte array"); log.info("Image successfully written to byte array");
} else { } else {
// Zip the images and return as byte array // Zip the images and return as byte array
try (ZipOutputStream zos = new ZipOutputStream(baos)) { try (ZipOutputStream zos = new ZipOutputStream(baos)) {
@@ -330,13 +328,13 @@ public class PdfUtils {
} }
} }
// Log that the images were successfully written to the byte array // Log that the images were successfully written to the byte array
logger.info("Images successfully written to byte array as a zip"); log.info("Images successfully written to byte array as a zip");
} }
} }
return baos.toByteArray(); return baos.toByteArray();
} catch (IOException e) { } catch (IOException e) {
// Log an error message if there is an issue converting the PDF to an image // Log an error message if there is an issue converting the PDF to an image
logger.error("Error converting PDF to image", e); log.error("Error converting PDF to image", e);
throw e; throw e;
} }
} }
@@ -354,12 +352,17 @@ public class PdfUtils {
pdfRenderer.setSubsamplingAllowed(true); pdfRenderer.setSubsamplingAllowed(true);
for (int page = 0; page < document.getNumberOfPages(); ++page) { for (int page = 0; page < document.getNumberOfPages(); ++page) {
BufferedImage bim = pdfRenderer.renderImageWithDPI(page, 300, ImageType.RGB); BufferedImage bim = pdfRenderer.renderImageWithDPI(page, 300, ImageType.RGB);
PDPage newPage = new PDPage(new PDRectangle(bim.getWidth(), bim.getHeight())); PDPage originalPage = document.getPage(page);
float width = originalPage.getMediaBox().getWidth();
float height = originalPage.getMediaBox().getHeight();
PDPage newPage = new PDPage(new PDRectangle(width, height));
imageDocument.addPage(newPage); imageDocument.addPage(newPage);
PDImageXObject pdImage = LosslessFactory.createFromImage(imageDocument, bim); PDImageXObject pdImage = LosslessFactory.createFromImage(imageDocument, bim);
PDPageContentStream contentStream = PDPageContentStream contentStream =
new PDPageContentStream(imageDocument, newPage, AppendMode.APPEND, true, true); new PDPageContentStream(imageDocument, newPage, AppendMode.APPEND, true, true);
contentStream.drawImage(pdImage, 0, 0); contentStream.drawImage(pdImage, 0, 0, width, height);
contentStream.close(); contentStream.close();
} }
return imageDocument; return imageDocument;
@@ -421,7 +424,7 @@ public class PdfUtils {
} }
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
doc.save(byteArrayOutputStream); doc.save(byteArrayOutputStream);
logger.info("PDF successfully saved to byte array"); log.info("PDF successfully saved to byte array");
return byteArrayOutputStream.toByteArray(); return byteArrayOutputStream.toByteArray();
} }
} }
@@ -471,7 +474,7 @@ public class PdfUtils {
image.getHeight() * scaleFactor); image.getHeight() * scaleFactor);
} }
} catch (IOException e) { } catch (IOException e) {
logger.error("Error adding image to PDF", e); log.error("Error adding image to PDF", e);
throw e; throw e;
} }
} }
@@ -498,20 +501,20 @@ public class PdfUtils {
PDImageXObject image = PDImageXObject.createFromByteArray(document, imageBytes, ""); PDImageXObject image = PDImageXObject.createFromByteArray(document, imageBytes, "");
// Draw the image onto the page at the specified x and y coordinates // Draw the image onto the page at the specified x and y coordinates
contentStream.drawImage(image, x, y); contentStream.drawImage(image, x, y);
logger.info("Image successfully overlayed onto PDF"); log.info("Image successfully overlayed onto PDF");
if (!everyPage && i == 0) { if (!everyPage && i == 0) {
break; break;
} }
} catch (IOException e) { } catch (IOException e) {
// Log an error message if there is an issue overlaying the image onto the PDF // Log an error message if there is an issue overlaying the image onto the PDF
logger.error("Error overlaying image onto PDF", e); log.error("Error overlaying image onto PDF", e);
throw e; throw e;
} }
} }
// Create a ByteArrayOutputStream to save the PDF to // Create a ByteArrayOutputStream to save the PDF to
ByteArrayOutputStream baos = new ByteArrayOutputStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream();
document.save(baos); document.save(baos);
logger.info("PDF successfully saved to byte array"); log.info("PDF successfully saved to byte array");
return baos.toByteArray(); return baos.toByteArray();
} }

View File

@@ -13,17 +13,14 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Semaphore; import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.github.pixee.security.BoundedLineReader; import io.github.pixee.security.BoundedLineReader;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.ApplicationProperties;
@Slf4j
public class ProcessExecutor { public class ProcessExecutor {
private static final Logger logger = LoggerFactory.getLogger(ProcessExecutor.class);
private static ApplicationProperties applicationProperties = new ApplicationProperties(); private static ApplicationProperties applicationProperties = new ApplicationProperties();
public enum Processes { public enum Processes {
@@ -160,7 +157,7 @@ public class ProcessExecutor {
semaphore.acquire(); semaphore.acquire();
try { try {
logger.info("Running command: " + String.join(" ", command)); log.info("Running command: " + String.join(" ", command));
ProcessBuilder processBuilder = new ProcessBuilder(command); ProcessBuilder processBuilder = new ProcessBuilder(command);
// Use the working directory if it's set // Use the working directory if it's set
@@ -187,13 +184,12 @@ public class ProcessExecutor {
errorReader, 5_000_000)) errorReader, 5_000_000))
!= null) { != null) {
errorLines.add(line); errorLines.add(line);
if (liveUpdates) logger.info(line); if (liveUpdates) log.info(line);
} }
} catch (InterruptedIOException e) { } catch (InterruptedIOException e) {
logger.warn( log.warn("Error reader thread was interrupted due to timeout.");
"Error reader thread was interrupted due to timeout.");
} catch (IOException e) { } catch (IOException e) {
logger.error("exception", e); log.error("exception", e);
} }
}); });
@@ -211,13 +207,12 @@ public class ProcessExecutor {
outputReader, 5_000_000)) outputReader, 5_000_000))
!= null) { != null) {
outputLines.add(line); outputLines.add(line);
if (liveUpdates) logger.info(line); if (liveUpdates) log.info(line);
} }
} catch (InterruptedIOException e) { } catch (InterruptedIOException e) {
logger.warn( log.warn("Error reader thread was interrupted due to timeout.");
"Error reader thread was interrupted due to timeout.");
} catch (IOException e) { } catch (IOException e) {
logger.error("exception", e); log.error("exception", e);
} }
}); });
@@ -244,7 +239,7 @@ public class ProcessExecutor {
String outputMessage = String.join("\n", outputLines); String outputMessage = String.join("\n", outputLines);
messages += outputMessage; messages += outputMessage;
if (!liveUpdates) { if (!liveUpdates) {
logger.info("Command output:\n" + outputMessage); log.info("Command output:\n" + outputMessage);
} }
} }
@@ -252,7 +247,7 @@ public class ProcessExecutor {
String errorMessage = String.join("\n", errorLines); String errorMessage = String.join("\n", errorLines);
messages += errorMessage; messages += errorMessage;
if (!liveUpdates) { if (!liveUpdates) {
logger.warn("Command error output:\n" + errorMessage); log.warn("Command error output:\n" + errorMessage);
} }
if (exitCode != 0) { if (exitCode != 0) {
throw new IOException( throw new IOException(

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -238,11 +238,13 @@ database.creationDate=تاريخ الإنشاء
database.fileSize=حجم الملف database.fileSize=حجم الملف
database.deleteBackupFile=حذف ملف النسخ الاحتياطي database.deleteBackupFile=حذف ملف النسخ الاحتياطي
database.importBackupFile=استيراد ملف النسخ الاحتياطي database.importBackupFile=استيراد ملف النسخ الاحتياطي
database.createBackupFile=Create Backup File
database.downloadBackupFile=تنزيل ملف النسخ الاحتياطي database.downloadBackupFile=تنزيل ملف النسخ الاحتياطي
database.info_1=عند استيراد البيانات، من الضروري ضمان الهيكل الصحيح. إذا كنت غير متأكد مما تفعله، اطلب المشورة والدعم من محترف. يمكن أن يؤدي الخطأ في الهيكل إلى حدوث أعطال في التطبيق، حتى عدم القدرة على تشغيل التطبيق بالكامل. database.info_1=عند استيراد البيانات، من الضروري ضمان الهيكل الصحيح. إذا كنت غير متأكد مما تفعله، اطلب المشورة والدعم من محترف. يمكن أن يؤدي الخطأ في الهيكل إلى حدوث أعطال في التطبيق، حتى عدم القدرة على تشغيل التطبيق بالكامل.
database.info_2=لا يهم اسم الملف عند التحميل. سيتم إعادة تسميته بعد ذلك لاتباع التنسيق backup_user_yyyyMMddHHmm.sql، مما يضمن اتساق تسمية متناسق. database.info_2=لا يهم اسم الملف عند التحميل. سيتم إعادة تسميته بعد ذلك لاتباع التنسيق backup_user_yyyyMMddHHmm.sql، مما يضمن اتساق تسمية متناسق.
database.submit=استيراد النسخة الاحتياطية database.submit=استيراد النسخة الاحتياطية
database.importIntoDatabaseSuccessed=تم استيراد قاعدة البيانات بنجاح database.importIntoDatabaseSuccessed=تم استيراد قاعدة البيانات بنجاح
database.backupCreated=Database backup successful
database.fileNotFound=لم يتم العثور على الملف database.fileNotFound=لم يتم العثور على الملف
database.fileNullOrEmpty=يجب ألا يكون الملف فارغًا أو خاليًا database.fileNullOrEmpty=يجب ألا يكون الملف فارغًا أو خاليًا
database.failedImportFile=فشل استيراد الملف database.failedImportFile=فشل استيراد الملف
@@ -512,6 +514,10 @@ home.splitPdfByChapters.title=تجزئة المستندات PDF حسب الفص
home.splitPdfByChapters.desc=قسم مستند PDF إلى ملفات متعددة بناءً على هيكل فصوله. home.splitPdfByChapters.desc=قسم مستند PDF إلى ملفات متعددة بناءً على هيكل فصوله.
splitPdfByChapters.tags=تجزئة، فصول، علامات تبويب، تنظيم splitPdfByChapters.tags=تجزئة، فصول، علامات تبويب، تنظيم
home.validateSignature.title=Validate PDF Signature
home.validateSignature.desc=Verify digital signatures and certificates in PDF documents
validateSignature.tags=signature,verify,validate,pdf,certificate,digital signature,Validate Signature,Validate certificate
#replace-invert-color #replace-invert-color
replace-color.title=إستبدال-عكس اللون replace-color.title=إستبدال-عكس اللون
replace-color.header=استبدال-عكس لون PDF replace-color.header=استبدال-عكس لون PDF
@@ -961,6 +967,16 @@ multiTool.dragDropMessage=الصفحات المحددة
multiTool.undo=تراجع multiTool.undo=تراجع
multiTool.redo=إعادة إجراء multiTool.redo=إعادة إجراء
#decrypt
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
decrypt.cancelled=Operation cancelled for PDF: {0}
decrypt.noPassword=No password provided for encrypted PDF: {0}
decrypt.invalidPassword=Please try again with the correct password.
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
decrypt.unexpectedError=There was an error processing the file. Please try again.
decrypt.serverError=Server error while decrypting: {0}
decrypt.success=File decrypted successfully.
#multiTool-advert #multiTool-advert
multiTool-advert.message=هذه الميزة متوفرة في <a href="{0}">صفحة الأدوات المتعددة</a> لدينا. اطلع عليها للحصول على واجهة مستخدم محسّنة لكل صفحة وميزات إضافية! multiTool-advert.message=هذه الميزة متوفرة في <a href="{0}">صفحة الأدوات المتعددة</a> لدينا. اطلع عليها للحصول على واجهة مستخدم محسّنة لكل صفحة وميزات إضافية!
@@ -1052,6 +1068,7 @@ addPassword.submit=تشفير
#watermark #watermark
watermark.title=إضافة علامة مائية watermark.title=إضافة علامة مائية
watermark.header=إضافة علامة مائية watermark.header=إضافة علامة مائية
watermark.customColor=لون نص مخصص
watermark.selectText.1=حدد PDF لإضافة العلامة المائية إليه: watermark.selectText.1=حدد PDF لإضافة العلامة المائية إليه:
watermark.selectText.2=نص العلامة المائية: watermark.selectText.2=نص العلامة المائية:
watermark.selectText.3=حجم الخط: watermark.selectText.3=حجم الخط:
@@ -1263,6 +1280,11 @@ splitByChapters.desc.3=تمثيل البيانات الأصلية: إذا تم
splitByChapters.desc.4=سماح بالتكرار: إذا تم اختياره، يسمح بوجود معاينات متعددة في الصفحة نفسها لخلق ملفات PDF منفصلة. splitByChapters.desc.4=سماح بالتكرار: إذا تم اختياره، يسمح بوجود معاينات متعددة في الصفحة نفسها لخلق ملفات PDF منفصلة.
splitByChapters.submit=تقطيع ملف PDF splitByChapters.submit=تقطيع ملف PDF
#File Chooser
fileChooser.click=انقر هنا
fileChooser.or=أو
fileChooser.dragAndDrop=قم بسحب الملفات وإفلاتها
fileChooser.hoveredDragAndDrop=قم بسحب المفات وإفلاتها هنا
#release notes #release notes
releases.footer=Releases releases.footer=Releases
@@ -1270,3 +1292,38 @@ releases.title=Release Notes
releases.header=Release Notes releases.header=Release Notes
releases.current.version=Current Release releases.current.version=Current Release
releases.note=Release notes are only available in English releases.note=Release notes are only available in English
#Validate Signature
validateSignature.title=Validate PDF Signatures
validateSignature.header=Validate Digital Signatures
validateSignature.selectPDF=Select signed PDF file
validateSignature.submit=Validate Signatures
validateSignature.results=Validation Results
validateSignature.status=Status
validateSignature.signer=Signer
validateSignature.date=Date
validateSignature.reason=Reason
validateSignature.location=Location
validateSignature.noSignatures=No digital signatures found in this document
validateSignature.status.valid=Valid
validateSignature.status.invalid=Invalid
validateSignature.chain.invalid=Certificate chain validation failed - cannot verify signer's identity
validateSignature.trust.invalid=Certificate not in trust store - source cannot be verified
validateSignature.cert.expired=Certificate has expired
validateSignature.cert.revoked=Certificate has been revoked
validateSignature.signature.info=Signature Information
validateSignature.signature=Signature
validateSignature.signature.mathValid=Signature is mathematically valid BUT:
validateSignature.selectCustomCert=Custom Certificate File X.509 (Optional)
validateSignature.cert.info=Certificate Details
validateSignature.cert.issuer=Issuer
validateSignature.cert.subject=Subject
validateSignature.cert.serialNumber=Serial Number
validateSignature.cert.validFrom=Valid From
validateSignature.cert.validUntil=Valid Until
validateSignature.cert.algorithm=Algorithm
validateSignature.cert.keySize=Key Size
validateSignature.cert.version=Version
validateSignature.cert.keyUsage=Key Usage
validateSignature.cert.selfSigned=Self-Signed
validateSignature.cert.bits=bits

View File

@@ -238,11 +238,13 @@ database.creationDate=Yaradılma tarixi
database.fileSize=Fayl Ölçüsü database.fileSize=Fayl Ölçüsü
database.deleteBackupFile=Yedək Faylını Sil database.deleteBackupFile=Yedək Faylını Sil
database.importBackupFile=Yedək Faylını Daxil Et database.importBackupFile=Yedək Faylını Daxil Et
database.createBackupFile=Create Backup File
database.downloadBackupFile=Yedək Faylını Yüklə database.downloadBackupFile=Yedək Faylını Yüklə
database.info_1=Məlumatı daxil edərkən doğru strukturun mövcudluğundan əmin olmaq vacibdir. Əgər nə etdiyinizdən əmin deyilsinizsə, professional birindən məsləhət və yardım alın. Strukturdakı xəta proqramdakı nasazlıqlardan proqramı çalışdırma qabiliyyətinin tamamilə aradan qalxmasına qədər bir sıra problemlərə səbəb ola bilər. database.info_1=Məlumatı daxil edərkən doğru strukturun mövcudluğundan əmin olmaq vacibdir. Əgər nə etdiyinizdən əmin deyilsinizsə, professional birindən məsləhət və yardım alın. Strukturdakı xəta proqramdakı nasazlıqlardan proqramı çalışdırma qabiliyyətinin tamamilə aradan qalxmasına qədər bir sıra problemlərə səbəb ola bilər.
database.info_2=Faylın adı fayl yüklənərkən önəmli deyildir. Faylın adı sonradan sabit adlandırmanın varlığından əmin olmaq məqsədilə backup_user_yyyyMMddHHmm.sql tərzində formata dəyişdiriləcəkdir. database.info_2=Faylın adı fayl yüklənərkən önəmli deyildir. Faylın adı sonradan sabit adlandırmanın varlığından əmin olmaq məqsədilə backup_user_yyyyMMddHHmm.sql tərzində formata dəyişdiriləcəkdir.
database.submit=Yedəkləməni Daxil Et database.submit=Yedəkləməni Daxil Et
database.importIntoDatabaseSuccessed=Verilənlər bazasına daxil etmə uğurla nəticələndi database.importIntoDatabaseSuccessed=Verilənlər bazasına daxil etmə uğurla nəticələndi
database.backupCreated=Database backup successful
database.fileNotFound=Fayl Tapılmadı database.fileNotFound=Fayl Tapılmadı
database.fileNullOrEmpty=Fayl boş və ya "null" olmamalıdır database.fileNullOrEmpty=Fayl boş və ya "null" olmamalıdır
database.failedImportFile=Faylı daxil etmək alınmadı database.failedImportFile=Faylı daxil etmək alınmadı
@@ -512,6 +514,10 @@ home.splitPdfByChapters.title=PDF-i Fəsillərə Əsasən Böl
home.splitPdfByChapters.desc=Fəsil strukturuna əsasən PDF-i bir neçə fayla böl. home.splitPdfByChapters.desc=Fəsil strukturuna əsasən PDF-i bir neçə fayla böl.
splitPdfByChapters.tags=böl,fəsillər,əlfəcinlər,nizamla splitPdfByChapters.tags=böl,fəsillər,əlfəcinlər,nizamla
home.validateSignature.title=Validate PDF Signature
home.validateSignature.desc=Verify digital signatures and certificates in PDF documents
validateSignature.tags=signature,verify,validate,pdf,certificate,digital signature,Validate Signature,Validate certificate
#replace-invert-color #replace-invert-color
replace-color.title=Qabaqcıl Rəng Seçimləri replace-color.title=Qabaqcıl Rəng Seçimləri
replace-color.header=PDF-də Rəngləri Dəyiş-Tərsinə Çevir replace-color.header=PDF-də Rəngləri Dəyiş-Tərsinə Çevir
@@ -961,6 +967,16 @@ multiTool.dragDropMessage=Seçilmiş Səhifə(lər)
multiTool.undo=Undo multiTool.undo=Undo
multiTool.redo=Redo multiTool.redo=Redo
#decrypt
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
decrypt.cancelled=Operation cancelled for PDF: {0}
decrypt.noPassword=No password provided for encrypted PDF: {0}
decrypt.invalidPassword=Please try again with the correct password.
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
decrypt.unexpectedError=There was an error processing the file. Please try again.
decrypt.serverError=Server error while decrypting: {0}
decrypt.success=File decrypted successfully.
#multiTool-advert #multiTool-advert
multiTool-advert.message=Bu xüsusiyyət bizim <a href="{0}">multi-alət səhifə</a>mizdə də mövcuddur. Əlavə xüsusiyyətlər və səhifə-səhifə interfeys üçün sınaqdan keçirin! multiTool-advert.message=Bu xüsusiyyət bizim <a href="{0}">multi-alət səhifə</a>mizdə də mövcuddur. Əlavə xüsusiyyətlər və səhifə-səhifə interfeys üçün sınaqdan keçirin!
@@ -1052,6 +1068,7 @@ addPassword.submit=Şifrlə
#watermark #watermark
watermark.title=Watermark Əlavə Et watermark.title=Watermark Əlavə Et
watermark.header=Watermark Əlavə Et watermark.header=Watermark Əlavə Et
watermark.customColor=Fərdi Mətn Rəngi
watermark.selectText.1=Watermark əlavə olunacaq PDF-i seç watermark.selectText.1=Watermark əlavə olunacaq PDF-i seç
watermark.selectText.2=Watermark Mətni: watermark.selectText.2=Watermark Mətni:
watermark.selectText.3=Şrift Ölçüsü: watermark.selectText.3=Şrift Ölçüsü:
@@ -1263,6 +1280,11 @@ splitByChapters.desc.3=Metadatanı daxil edin: Əgər yoxlanılıbsa, orijinal P
splitByChapters.desc.4=Allow Duplicates: Dublikatlara icazə verin: Əgər işarələnərsə, eyni səhifədə birdən çox bookmarka ayrı-ayrı PDF sənədləri yaratmağa icazə verin. splitByChapters.desc.4=Allow Duplicates: Dublikatlara icazə verin: Əgər işarələnərsə, eyni səhifədə birdən çox bookmarka ayrı-ayrı PDF sənədləri yaratmağa icazə verin.
splitByChapters.submit=PDF-i Ayır splitByChapters.submit=PDF-i Ayır
#File Chooser
fileChooser.click=Click
fileChooser.or=or
fileChooser.dragAndDrop=Drag & Drop
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
#release notes #release notes
releases.footer=Buraxılışlar releases.footer=Buraxılışlar
@@ -1270,3 +1292,38 @@ releases.title=Buraxılış Qeydləri
releases.header=Buraxılış Qeydləri releases.header=Buraxılış Qeydləri
releases.current.version=Hazırki Buraxılış releases.current.version=Hazırki Buraxılış
releases.note=Buraxılış Qeydləri yalnız ingiliscə mövcuddur releases.note=Buraxılış Qeydləri yalnız ingiliscə mövcuddur
#Validate Signature
validateSignature.title=Validate PDF Signatures
validateSignature.header=Validate Digital Signatures
validateSignature.selectPDF=Select signed PDF file
validateSignature.submit=Validate Signatures
validateSignature.results=Validation Results
validateSignature.status=Status
validateSignature.signer=Signer
validateSignature.date=Date
validateSignature.reason=Reason
validateSignature.location=Location
validateSignature.noSignatures=No digital signatures found in this document
validateSignature.status.valid=Valid
validateSignature.status.invalid=Invalid
validateSignature.chain.invalid=Certificate chain validation failed - cannot verify signer's identity
validateSignature.trust.invalid=Certificate not in trust store - source cannot be verified
validateSignature.cert.expired=Certificate has expired
validateSignature.cert.revoked=Certificate has been revoked
validateSignature.signature.info=Signature Information
validateSignature.signature=Signature
validateSignature.signature.mathValid=Signature is mathematically valid BUT:
validateSignature.selectCustomCert=Custom Certificate File X.509 (Optional)
validateSignature.cert.info=Certificate Details
validateSignature.cert.issuer=Issuer
validateSignature.cert.subject=Subject
validateSignature.cert.serialNumber=Serial Number
validateSignature.cert.validFrom=Valid From
validateSignature.cert.validUntil=Valid Until
validateSignature.cert.algorithm=Algorithm
validateSignature.cert.keySize=Key Size
validateSignature.cert.version=Version
validateSignature.cert.keyUsage=Key Usage
validateSignature.cert.selfSigned=Self-Signed
validateSignature.cert.bits=bits

View File

@@ -238,11 +238,13 @@ database.creationDate=Дата на създаване
database.fileSize=Размер на файла database.fileSize=Размер на файла
database.deleteBackupFile=Изтриване на архивен файл database.deleteBackupFile=Изтриване на архивен файл
database.importBackupFile=Импортиране на архивен файл database.importBackupFile=Импортиране на архивен файл
database.createBackupFile=Create Backup File
database.downloadBackupFile=Изтеглете архивен файл database.downloadBackupFile=Изтеглете архивен файл
database.info_1=Когато импортирате данни, е от решаващо значение да осигурите правилната структура. Ако не сте сигурни в това, което правите, потърсете съвет и подкрепа от професионалист. Грешка в структурата може да причини неизправност на приложението, включително пълна невъзможност за стартиране на приложението. database.info_1=Когато импортирате данни, е от решаващо значение да осигурите правилната структура. Ако не сте сигурни в това, което правите, потърсете съвет и подкрепа от професионалист. Грешка в структурата може да причини неизправност на приложението, включително пълна невъзможност за стартиране на приложението.
database.info_2=Името на файла няма значение при качване. След това ще бъде преименуван, за да следва формата backup_user_yyyyMMddHHmm.sql, осигурявайки последователна конвенция за именуване. database.info_2=Името на файла няма значение при качване. След това ще бъде преименуван, за да следва формата backup_user_yyyyMMddHHmm.sql, осигурявайки последователна конвенция за именуване.
database.submit=Импортиране на резервно копие database.submit=Импортиране на резервно копие
database.importIntoDatabaseSuccessed=Импортирането в базата данни бе успешно database.importIntoDatabaseSuccessed=Импортирането в базата данни бе успешно
database.backupCreated=Database backup successful
database.fileNotFound=Файлът не е намерен database.fileNotFound=Файлът не е намерен
database.fileNullOrEmpty=Файлът не трябва да е нулев или празен database.fileNullOrEmpty=Файлът не трябва да е нулев или празен
database.failedImportFile=Неуспешно импортиране на файл database.failedImportFile=Неуспешно импортиране на файл
@@ -512,6 +514,10 @@ home.splitPdfByChapters.title=Разделете PDF по глави
home.splitPdfByChapters.desc=Разделете PDF на множество файлове въз основа на неговата структура на глави. home.splitPdfByChapters.desc=Разделете PDF на множество файлове въз основа на неговата структура на глави.
splitPdfByChapters.tags=разделяне, глави, отметки, организиране splitPdfByChapters.tags=разделяне, глави, отметки, организиране
home.validateSignature.title=Validate PDF Signature
home.validateSignature.desc=Verify digital signatures and certificates in PDF documents
validateSignature.tags=signature,verify,validate,pdf,certificate,digital signature,Validate Signature,Validate certificate
#replace-invert-color #replace-invert-color
replace-color.title=Замени-инвертиране-на-цвят replace-color.title=Замени-инвертиране-на-цвят
replace-color.header=Замяна-инвертиране на цвят PDF replace-color.header=Замяна-инвертиране на цвят PDF
@@ -961,6 +967,16 @@ multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo multiTool.undo=Undo
multiTool.redo=Redo multiTool.redo=Redo
#decrypt
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
decrypt.cancelled=Operation cancelled for PDF: {0}
decrypt.noPassword=No password provided for encrypted PDF: {0}
decrypt.invalidPassword=Please try again with the correct password.
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
decrypt.unexpectedError=There was an error processing the file. Please try again.
decrypt.serverError=Server error while decrypting: {0}
decrypt.success=File decrypted successfully.
#multiTool-advert #multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features! multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1052,6 +1068,7 @@ addPassword.submit=Шифроване
#watermark #watermark
watermark.title=Добавяне на воден знак watermark.title=Добавяне на воден знак
watermark.header=Добавяне на воден знак watermark.header=Добавяне на воден знак
watermark.customColor=Персонализиран цвят на текста
watermark.selectText.1=Изберете PDF, към който да добавите воден знак: watermark.selectText.1=Изберете PDF, към който да добавите воден знак:
watermark.selectText.2=Текст на воден знак: watermark.selectText.2=Текст на воден знак:
watermark.selectText.3=Размер на шрифта: watermark.selectText.3=Размер на шрифта:
@@ -1263,6 +1280,11 @@ splitByChapters.desc.3=Включване на метаданни: Ако е о
splitByChapters.desc.4=Разрешаване на дубликати: Ако е отметнато, позволява множество отметки на една и съща страница за създаване на отделни PDF файлове. splitByChapters.desc.4=Разрешаване на дубликати: Ако е отметнато, позволява множество отметки на една и съща страница за създаване на отделни PDF файлове.
splitByChapters.submit=Разделяне на PDF splitByChapters.submit=Разделяне на PDF
#File Chooser
fileChooser.click=Click
fileChooser.or=or
fileChooser.dragAndDrop=Drag & Drop
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
#release notes #release notes
releases.footer=Releases releases.footer=Releases
@@ -1270,3 +1292,38 @@ releases.title=Release Notes
releases.header=Release Notes releases.header=Release Notes
releases.current.version=Current Release releases.current.version=Current Release
releases.note=Release notes are only available in English releases.note=Release notes are only available in English
#Validate Signature
validateSignature.title=Validate PDF Signatures
validateSignature.header=Validate Digital Signatures
validateSignature.selectPDF=Select signed PDF file
validateSignature.submit=Validate Signatures
validateSignature.results=Validation Results
validateSignature.status=Status
validateSignature.signer=Signer
validateSignature.date=Date
validateSignature.reason=Reason
validateSignature.location=Location
validateSignature.noSignatures=No digital signatures found in this document
validateSignature.status.valid=Valid
validateSignature.status.invalid=Invalid
validateSignature.chain.invalid=Certificate chain validation failed - cannot verify signer's identity
validateSignature.trust.invalid=Certificate not in trust store - source cannot be verified
validateSignature.cert.expired=Certificate has expired
validateSignature.cert.revoked=Certificate has been revoked
validateSignature.signature.info=Signature Information
validateSignature.signature=Signature
validateSignature.signature.mathValid=Signature is mathematically valid BUT:
validateSignature.selectCustomCert=Custom Certificate File X.509 (Optional)
validateSignature.cert.info=Certificate Details
validateSignature.cert.issuer=Issuer
validateSignature.cert.subject=Subject
validateSignature.cert.serialNumber=Serial Number
validateSignature.cert.validFrom=Valid From
validateSignature.cert.validUntil=Valid Until
validateSignature.cert.algorithm=Algorithm
validateSignature.cert.keySize=Key Size
validateSignature.cert.version=Version
validateSignature.cert.keyUsage=Key Usage
validateSignature.cert.selfSigned=Self-Signed
validateSignature.cert.bits=bits

View File

@@ -238,11 +238,13 @@ database.creationDate=Data de Creació
database.fileSize=Mida del Fitxer database.fileSize=Mida del Fitxer
database.deleteBackupFile=Elimina el Fitxer de Còpia de Seguretat database.deleteBackupFile=Elimina el Fitxer de Còpia de Seguretat
database.importBackupFile=Importa el Fitxer de Còpia de Seguretat database.importBackupFile=Importa el Fitxer de Còpia de Seguretat
database.createBackupFile=Create Backup File
database.downloadBackupFile=Descarrega el Fitxer de Còpia de Seguretat database.downloadBackupFile=Descarrega el Fitxer de Còpia de Seguretat
database.info_1=Quan importis dades, és crucial assegurar-se que l'estructura sigui correcta. Si no estàs segur del que fas, busca l'assessorament d'un professional. Un error en l'estructura pot causar malfuncionaments de l'aplicació, fins i tot impossibilitar-ne l'execució. database.info_1=Quan importis dades, és crucial assegurar-se que l'estructura sigui correcta. Si no estàs segur del que fas, busca l'assessorament d'un professional. Un error en l'estructura pot causar malfuncionaments de l'aplicació, fins i tot impossibilitar-ne l'execució.
database.info_2=El nom del fitxer no importa quan es puja. Es renombrarà després per seguir el format backup_user_yyyyMMddHHmm.sql, assegurant una convenció de nomenclatura consistent. database.info_2=El nom del fitxer no importa quan es puja. Es renombrarà després per seguir el format backup_user_yyyyMMddHHmm.sql, assegurant una convenció de nomenclatura consistent.
database.submit=Importa la Còpia de Seguretat database.submit=Importa la Còpia de Seguretat
database.importIntoDatabaseSuccessed=Importació a la base de dades completada amb èxit database.importIntoDatabaseSuccessed=Importació a la base de dades completada amb èxit
database.backupCreated=Database backup successful
database.fileNotFound=Fitxer no trobat database.fileNotFound=Fitxer no trobat
database.fileNullOrEmpty=El fitxer no ha de ser nul o buit database.fileNullOrEmpty=El fitxer no ha de ser nul o buit
database.failedImportFile=Error en la importació del fitxer database.failedImportFile=Error en la importació del fitxer
@@ -512,6 +514,10 @@ home.splitPdfByChapters.title=Divideix PDF per Capítols
home.splitPdfByChapters.desc=Divideix un PDF en múltiples fitxers segons la seva estructura de capítols. home.splitPdfByChapters.desc=Divideix un PDF en múltiples fitxers segons la seva estructura de capítols.
splitPdfByChapters.tags=split,chapters,bookmarks,organize splitPdfByChapters.tags=split,chapters,bookmarks,organize
home.validateSignature.title=Validate PDF Signature
home.validateSignature.desc=Verify digital signatures and certificates in PDF documents
validateSignature.tags=signature,verify,validate,pdf,certificate,digital signature,Validate Signature,Validate certificate
#replace-invert-color #replace-invert-color
replace-color.title=Reemplaça-Inverteix-Color replace-color.title=Reemplaça-Inverteix-Color
replace-color.header=Reemplaça-Inverteix Color en PDF replace-color.header=Reemplaça-Inverteix Color en PDF
@@ -961,6 +967,16 @@ multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo multiTool.undo=Undo
multiTool.redo=Redo multiTool.redo=Redo
#decrypt
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
decrypt.cancelled=Operation cancelled for PDF: {0}
decrypt.noPassword=No password provided for encrypted PDF: {0}
decrypt.invalidPassword=Please try again with the correct password.
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
decrypt.unexpectedError=There was an error processing the file. Please try again.
decrypt.serverError=Server error while decrypting: {0}
decrypt.success=File decrypted successfully.
#multiTool-advert #multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features! multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1052,6 +1068,7 @@ addPassword.submit=Encripta
#watermark #watermark
watermark.title=Afegir Marca d'Aigua watermark.title=Afegir Marca d'Aigua
watermark.header=Afegir Marca d'Aigua watermark.header=Afegir Marca d'Aigua
watermark.customColor=Color de Text Personalitzat
watermark.selectText.1=Selecciona el PDF per afegir la Marca d'Aigua: watermark.selectText.1=Selecciona el PDF per afegir la Marca d'Aigua:
watermark.selectText.2=Text de la Marca d'Aigua watermark.selectText.2=Text de la Marca d'Aigua
watermark.selectText.3=Mida de la Font: watermark.selectText.3=Mida de la Font:
@@ -1263,6 +1280,11 @@ splitByChapters.desc.3=Incloure Metadades: Si està marcat, les metadades del PD
splitByChapters.desc.4=Permetre Duplicats: Si està marcat, permet diversos marcadors a la mateixa pàgina per crear PDFs separats. splitByChapters.desc.4=Permetre Duplicats: Si està marcat, permet diversos marcadors a la mateixa pàgina per crear PDFs separats.
splitByChapters.submit=Divideix PDF splitByChapters.submit=Divideix PDF
#File Chooser
fileChooser.click=Click
fileChooser.or=or
fileChooser.dragAndDrop=Drag & Drop
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
#release notes #release notes
releases.footer=Releases releases.footer=Releases
@@ -1270,3 +1292,38 @@ releases.title=Release Notes
releases.header=Release Notes releases.header=Release Notes
releases.current.version=Current Release releases.current.version=Current Release
releases.note=Release notes are only available in English releases.note=Release notes are only available in English
#Validate Signature
validateSignature.title=Validate PDF Signatures
validateSignature.header=Validate Digital Signatures
validateSignature.selectPDF=Select signed PDF file
validateSignature.submit=Validate Signatures
validateSignature.results=Validation Results
validateSignature.status=Status
validateSignature.signer=Signer
validateSignature.date=Date
validateSignature.reason=Reason
validateSignature.location=Location
validateSignature.noSignatures=No digital signatures found in this document
validateSignature.status.valid=Valid
validateSignature.status.invalid=Invalid
validateSignature.chain.invalid=Certificate chain validation failed - cannot verify signer's identity
validateSignature.trust.invalid=Certificate not in trust store - source cannot be verified
validateSignature.cert.expired=Certificate has expired
validateSignature.cert.revoked=Certificate has been revoked
validateSignature.signature.info=Signature Information
validateSignature.signature=Signature
validateSignature.signature.mathValid=Signature is mathematically valid BUT:
validateSignature.selectCustomCert=Custom Certificate File X.509 (Optional)
validateSignature.cert.info=Certificate Details
validateSignature.cert.issuer=Issuer
validateSignature.cert.subject=Subject
validateSignature.cert.serialNumber=Serial Number
validateSignature.cert.validFrom=Valid From
validateSignature.cert.validUntil=Valid Until
validateSignature.cert.algorithm=Algorithm
validateSignature.cert.keySize=Key Size
validateSignature.cert.version=Version
validateSignature.cert.keyUsage=Key Usage
validateSignature.cert.selfSigned=Self-Signed
validateSignature.cert.bits=bits

View File

@@ -238,11 +238,13 @@ database.creationDate=Datum vytvoření
database.fileSize=Velikost souboru database.fileSize=Velikost souboru
database.deleteBackupFile=Smazat záložní soubor database.deleteBackupFile=Smazat záložní soubor
database.importBackupFile=Import zálohy souboru database.importBackupFile=Import zálohy souboru
database.createBackupFile=Create Backup File
database.downloadBackupFile=Stáhnout zálohový soubor database.downloadBackupFile=Stáhnout zálohový soubor
database.info_1=Při importu dat je důležité zajistit správnou strukturu. Pokud jste nejistí, jak se chovat, hledajte konzultaci a podporu od profesionala. Chyba v struktuře může vést k selhání aplikace, dokonce i k tomu, že by aplikace nemohla být spuštěna. database.info_1=Při importu dat je důležité zajistit správnou strukturu. Pokud jste nejistí, jak se chovat, hledajte konzultaci a podporu od profesionala. Chyba v struktuře může vést k selhání aplikace, dokonce i k tomu, že by aplikace nemohla být spuštěna.
database.info_2=Název souboru nezáleží při nahrávání. Bude jeho zpětně znovu pojmenován podle formáту backup_user_yyyyMMddHHmm.sql, což zajišťuje konzistentní pravidlo označení. database.info_2=Název souboru nezáleží při nahrávání. Bude jeho zpětně znovu pojmenován podle formáту backup_user_yyyyMMddHHmm.sql, což zajišťuje konzistentní pravidlo označení.
database.submit=Import zálohy database.submit=Import zálohy
database.importIntoDatabaseSuccessed=Import do databáze byl úspěšný database.importIntoDatabaseSuccessed=Import do databáze byl úspěšný
database.backupCreated=Database backup successful
database.fileNotFound=File not Found database.fileNotFound=File not Found
database.fileNullOrEmpty=Soubor nemůže být null nebo prázdný database.fileNullOrEmpty=Soubor nemůže být null nebo prázdný
database.failedImportFile=Failed Import File database.failedImportFile=Failed Import File
@@ -512,6 +514,10 @@ home.splitPdfByChapters.title=Rozdělit PDF podle kapitol
home.splitPdfByChapters.desc=Rozdělit PDF do více souborů na základě jeho struktury kapitol. home.splitPdfByChapters.desc=Rozdělit PDF do více souborů na základě jeho struktury kapitol.
splitPdfByChapters.tags=rozdělení, kapitoly, zápisky, organizace splitPdfByChapters.tags=rozdělení, kapitoly, zápisky, organizace
home.validateSignature.title=Validate PDF Signature
home.validateSignature.desc=Verify digital signatures and certificates in PDF documents
validateSignature.tags=signature,verify,validate,pdf,certificate,digital signature,Validate Signature,Validate certificate
#replace-invert-color #replace-invert-color
replace-color.title=Replace-Invert-Color replace-color.title=Replace-Invert-Color
replace-color.header=Nahradit inverzní barvu PDF replace-color.header=Nahradit inverzní barvu PDF
@@ -961,6 +967,16 @@ multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo multiTool.undo=Undo
multiTool.redo=Redo multiTool.redo=Redo
#decrypt
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
decrypt.cancelled=Operation cancelled for PDF: {0}
decrypt.noPassword=No password provided for encrypted PDF: {0}
decrypt.invalidPassword=Please try again with the correct password.
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
decrypt.unexpectedError=There was an error processing the file. Please try again.
decrypt.serverError=Server error while decrypting: {0}
decrypt.success=File decrypted successfully.
#multiTool-advert #multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features! multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1052,6 +1068,7 @@ addPassword.submit=Šifrovat
#watermark #watermark
watermark.title=Přidat vodoznak watermark.title=Přidat vodoznak
watermark.header=Přidat vodoznak watermark.header=Přidat vodoznak
watermark.customColor=Vlastní barva textu
watermark.selectText.1=Vyberte PDF, ke kterému chcete přidat vodoznak: watermark.selectText.1=Vyberte PDF, ke kterému chcete přidat vodoznak:
watermark.selectText.2=Text vodoznaku: watermark.selectText.2=Text vodoznaku:
watermark.selectText.3=Velikost písma: watermark.selectText.3=Velikost písma:
@@ -1263,6 +1280,11 @@ splitByChapters.desc.3=Zahrnout metadatů: Pokud je zaškrtnuto, původní metad
splitByChapters.desc.4=Povolit duplicitní záznamy: Pokud je zaškrtnuto, návštěvníci mohou vytvořit samostatné PDF soubory z více záhlaví na stejné straně. splitByChapters.desc.4=Povolit duplicitní záznamy: Pokud je zaškrtnuto, návštěvníci mohou vytvořit samostatné PDF soubory z více záhlaví na stejné straně.
splitByChapters.submit=Podělit se PDF splitByChapters.submit=Podělit se PDF
#File Chooser
fileChooser.click=Click
fileChooser.or=or
fileChooser.dragAndDrop=Drag & Drop
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
#release notes #release notes
releases.footer=Releases releases.footer=Releases
@@ -1270,3 +1292,38 @@ releases.title=Release Notes
releases.header=Release Notes releases.header=Release Notes
releases.current.version=Current Release releases.current.version=Current Release
releases.note=Release notes are only available in English releases.note=Release notes are only available in English
#Validate Signature
validateSignature.title=Validate PDF Signatures
validateSignature.header=Validate Digital Signatures
validateSignature.selectPDF=Select signed PDF file
validateSignature.submit=Validate Signatures
validateSignature.results=Validation Results
validateSignature.status=Status
validateSignature.signer=Signer
validateSignature.date=Date
validateSignature.reason=Reason
validateSignature.location=Location
validateSignature.noSignatures=No digital signatures found in this document
validateSignature.status.valid=Valid
validateSignature.status.invalid=Invalid
validateSignature.chain.invalid=Certificate chain validation failed - cannot verify signer's identity
validateSignature.trust.invalid=Certificate not in trust store - source cannot be verified
validateSignature.cert.expired=Certificate has expired
validateSignature.cert.revoked=Certificate has been revoked
validateSignature.signature.info=Signature Information
validateSignature.signature=Signature
validateSignature.signature.mathValid=Signature is mathematically valid BUT:
validateSignature.selectCustomCert=Custom Certificate File X.509 (Optional)
validateSignature.cert.info=Certificate Details
validateSignature.cert.issuer=Issuer
validateSignature.cert.subject=Subject
validateSignature.cert.serialNumber=Serial Number
validateSignature.cert.validFrom=Valid From
validateSignature.cert.validUntil=Valid Until
validateSignature.cert.algorithm=Algorithm
validateSignature.cert.keySize=Key Size
validateSignature.cert.version=Version
validateSignature.cert.keyUsage=Key Usage
validateSignature.cert.selfSigned=Self-Signed
validateSignature.cert.bits=bits

View File

@@ -238,11 +238,13 @@ database.creationDate=Oprettelsesdato
database.fileSize=Filstørrelse database.fileSize=Filstørrelse
database.deleteBackupFile=Slet Backup-fil database.deleteBackupFile=Slet Backup-fil
database.importBackupFile=Importér Backup-fil database.importBackupFile=Importér Backup-fil
database.createBackupFile=Create Backup File
database.downloadBackupFile=Download Backup-fil database.downloadBackupFile=Download Backup-fil
database.info_1=Ved import af data er det afgørende at sikre den korrekte struktur. Hvis du er usikker på, hvad du gør, søg råd og støtte fra en professionel. En fejl i strukturen kan forårsage applikationsfejl, op til og med fuldstændig manglende evne til at køre applikationen. database.info_1=Ved import af data er det afgørende at sikre den korrekte struktur. Hvis du er usikker på, hvad du gør, søg råd og støtte fra en professionel. En fejl i strukturen kan forårsage applikationsfejl, op til og med fuldstændig manglende evne til at køre applikationen.
database.info_2=Filnavnet er ligegyldigt ved upload. Det vil blive omdøbt bagefter for at følge formatet backup_user_yyyyMMddHHmm.sql, hvilket sikrer en konsistent navngivningskonvention. database.info_2=Filnavnet er ligegyldigt ved upload. Det vil blive omdøbt bagefter for at følge formatet backup_user_yyyyMMddHHmm.sql, hvilket sikrer en konsistent navngivningskonvention.
database.submit=Importér Backup database.submit=Importér Backup
database.importIntoDatabaseSuccessed=Import i database lykkedes database.importIntoDatabaseSuccessed=Import i database lykkedes
database.backupCreated=Database backup successful
database.fileNotFound=Fil ikke fundet database.fileNotFound=Fil ikke fundet
database.fileNullOrEmpty=Fil må ikke være null eller tom database.fileNullOrEmpty=Fil må ikke være null eller tom
database.failedImportFile=Kunne ikke importere fil database.failedImportFile=Kunne ikke importere fil
@@ -512,6 +514,10 @@ home.splitPdfByChapters.title=Partitioner PDF efter kapitler
home.splitPdfByChapters.desc=Partitioner en PDF i flere filer baseret på dens kapitelstruktur. home.splitPdfByChapters.desc=Partitioner en PDF i flere filer baseret på dens kapitelstruktur.
splitPdfByChapters.tags=partitionering,kapitler,merker,organisering splitPdfByChapters.tags=partitionering,kapitler,merker,organisering
home.validateSignature.title=Validate PDF Signature
home.validateSignature.desc=Verify digital signatures and certificates in PDF documents
validateSignature.tags=signature,verify,validate,pdf,certificate,digital signature,Validate Signature,Validate certificate
#replace-invert-color #replace-invert-color
replace-color.title=Replace-Invert-Color replace-color.title=Replace-Invert-Color
replace-color.header=Erstat-omgivende Farve PDF replace-color.header=Erstat-omgivende Farve PDF
@@ -961,6 +967,16 @@ multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo multiTool.undo=Undo
multiTool.redo=Redo multiTool.redo=Redo
#decrypt
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
decrypt.cancelled=Operation cancelled for PDF: {0}
decrypt.noPassword=No password provided for encrypted PDF: {0}
decrypt.invalidPassword=Please try again with the correct password.
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
decrypt.unexpectedError=There was an error processing the file. Please try again.
decrypt.serverError=Server error while decrypting: {0}
decrypt.success=File decrypted successfully.
#multiTool-advert #multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features! multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1052,6 +1068,7 @@ addPassword.submit=Kryptér
#watermark #watermark
watermark.title=Tilføj Vandmærke watermark.title=Tilføj Vandmærke
watermark.header=Tilføj Vandmærke watermark.header=Tilføj Vandmærke
watermark.customColor=Brugerdefineret Tekstfarve
watermark.selectText.1=Vælg PDF til at tilføje vandmærke: watermark.selectText.1=Vælg PDF til at tilføje vandmærke:
watermark.selectText.2=Vandmærketekst: watermark.selectText.2=Vandmærketekst:
watermark.selectText.3=Skriftstørrelse: watermark.selectText.3=Skriftstørrelse:
@@ -1263,6 +1280,11 @@ splitByChapters.desc.3=Inkluder metadata: Hvis markeret, vil den originale PDF's
splitByChapters.desc.4=Tillad duplikater: Hvis markeret, tillader det flere bogmærker på samme side til at oprette separate PDF'er. splitByChapters.desc.4=Tillad duplikater: Hvis markeret, tillader det flere bogmærker på samme side til at oprette separate PDF'er.
splitByChapters.submit=Splitter PDF splitByChapters.submit=Splitter PDF
#File Chooser
fileChooser.click=Click
fileChooser.or=or
fileChooser.dragAndDrop=Drag & Drop
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
#release notes #release notes
releases.footer=Releases releases.footer=Releases
@@ -1270,3 +1292,38 @@ releases.title=Release Notes
releases.header=Release Notes releases.header=Release Notes
releases.current.version=Current Release releases.current.version=Current Release
releases.note=Release notes are only available in English releases.note=Release notes are only available in English
#Validate Signature
validateSignature.title=Validate PDF Signatures
validateSignature.header=Validate Digital Signatures
validateSignature.selectPDF=Select signed PDF file
validateSignature.submit=Validate Signatures
validateSignature.results=Validation Results
validateSignature.status=Status
validateSignature.signer=Signer
validateSignature.date=Date
validateSignature.reason=Reason
validateSignature.location=Location
validateSignature.noSignatures=No digital signatures found in this document
validateSignature.status.valid=Valid
validateSignature.status.invalid=Invalid
validateSignature.chain.invalid=Certificate chain validation failed - cannot verify signer's identity
validateSignature.trust.invalid=Certificate not in trust store - source cannot be verified
validateSignature.cert.expired=Certificate has expired
validateSignature.cert.revoked=Certificate has been revoked
validateSignature.signature.info=Signature Information
validateSignature.signature=Signature
validateSignature.signature.mathValid=Signature is mathematically valid BUT:
validateSignature.selectCustomCert=Custom Certificate File X.509 (Optional)
validateSignature.cert.info=Certificate Details
validateSignature.cert.issuer=Issuer
validateSignature.cert.subject=Subject
validateSignature.cert.serialNumber=Serial Number
validateSignature.cert.validFrom=Valid From
validateSignature.cert.validUntil=Valid Until
validateSignature.cert.algorithm=Algorithm
validateSignature.cert.keySize=Key Size
validateSignature.cert.version=Version
validateSignature.cert.keyUsage=Key Usage
validateSignature.cert.selfSigned=Self-Signed
validateSignature.cert.bits=bits

View File

@@ -146,8 +146,8 @@ navbar.search=Suche
navbar.sections.organize=Organisieren navbar.sections.organize=Organisieren
navbar.sections.convertTo=In PDF konvertieren navbar.sections.convertTo=In PDF konvertieren
navbar.sections.convertFrom=Konvertieren von PDF navbar.sections.convertFrom=Konvertieren von PDF
navbar.sections.security=Zeichen und Sicherheit navbar.sections.security=Signieren und Sicherheit
navbar.sections.advance=Fortschrittlich navbar.sections.advance=Erweiterte Funktionen
navbar.sections.edit=Anzeigen und Bearbeiten navbar.sections.edit=Anzeigen und Bearbeiten
navbar.sections.popular=Beliebt navbar.sections.popular=Beliebt
@@ -238,17 +238,19 @@ database.creationDate=Erstellungsdatum
database.fileSize=Dateigröße database.fileSize=Dateigröße
database.deleteBackupFile=Sicherungsdatei löschen database.deleteBackupFile=Sicherungsdatei löschen
database.importBackupFile=Sicherungsdatei importieren database.importBackupFile=Sicherungsdatei importieren
database.createBackupFile=Sicherungsdatei erstellen
database.downloadBackupFile=Sicherungsdatei herunterladen database.downloadBackupFile=Sicherungsdatei herunterladen
database.info_1=Beim Importieren der Daten ist es von größter Bedeutung, die korrekte Struktur zu gewährleisten. Wenn Sie nicht sicher sind, was Sie tun, suchen Sie Rat und Unterstützung von einem Fachmann. Ein Fehler in der Struktur kann zu Fehlfunktionen der Anwendung führen, bis hin zur vollständigen Nicht-Lauffähigkeit der Anwendung. database.info_1=Beim Importieren der Daten ist es von größter Bedeutung, die korrekte Struktur zu gewährleisten. Wenn Sie nicht sicher sind, was Sie tun, suchen Sie Rat und Unterstützung von einem Fachmann. Ein Fehler in der Struktur kann zu Fehlfunktionen der Anwendung führen, bis hin zur vollständigen Nicht-Lauffähigkeit der Anwendung.
database.info_2=Der Dateiname spielt beim Hochladen keine Rolle. Dieser wird nachträglich in das Format backup_user_yyyyMMddHHmm.sql geändert, um eine einheitliche Benennung zu gewährleisten. database.info_2=Der Dateiname spielt beim Hochladen keine Rolle. Dieser wird nachträglich in das Format backup_user_yyyyMMddHHmm.sql geändert, um eine einheitliche Benennung zu gewährleisten.
database.submit=Sicherungsdatei importieren database.submit=Sicherungsdatei importieren
database.importIntoDatabaseSuccessed=Import in die Datenbank erfolgreich database.importIntoDatabaseSuccessed=Import in die Datenbank erfolgreich
database.backupCreated=Datenbanksicherung erfolgreich
database.fileNotFound=Datei nicht gefunden database.fileNotFound=Datei nicht gefunden
database.fileNullOrEmpty=Datei darf nicht null oder leer sein database.fileNullOrEmpty=Datei darf nicht null oder leer sein
database.failedImportFile=Dateiimport fehlgeschlagen database.failedImportFile=Dateiimport fehlgeschlagen
session.expired=Ihre Sitzung ist abgelaufen. Bitte laden Sie die Seite neu und versuchen Sie es erneut. session.expired=Ihre Sitzung ist abgelaufen. Bitte laden Sie die Seite neu und versuchen Sie es erneut.
session.refreshPage=Refresh Page session.refreshPage=Seite aktualisieren
############# #############
# HOME-PAGE # # HOME-PAGE #
@@ -512,6 +514,10 @@ home.splitPdfByChapters.title=PDF-Datei nach Kapiteln aufteilen
home.splitPdfByChapters.desc=Aufteilung einer PDF-Datei in mehrere Dateien auf Basis der Kapitelstruktur. home.splitPdfByChapters.desc=Aufteilung einer PDF-Datei in mehrere Dateien auf Basis der Kapitelstruktur.
splitPdfByChapters.tags=aufteilen,kapitel,lesezeichen,organisieren splitPdfByChapters.tags=aufteilen,kapitel,lesezeichen,organisieren
home.validateSignature.title=PDF-Signatur überprüfen
home.validateSignature.desc=Digitale Signaturen und Zertifikate in PDF-Dokumenten überprüfen
validateSignature.tags=signature,verify,validate,pdf,digitale signatur,signatur validieren,überprüfen,Zertifikat,cert
#replace-invert-color #replace-invert-color
replace-color.title=Farbe Ersetzen-Invertieren replace-color.title=Farbe Ersetzen-Invertieren
replace-color.header=Farb-PDF Ersetzen-Invertieren replace-color.header=Farb-PDF Ersetzen-Invertieren
@@ -818,12 +824,12 @@ sign.save=Signature speichern
sign.personalSigs=Persönliche Signaturen sign.personalSigs=Persönliche Signaturen
sign.sharedSigs=Geteilte Signaturen sign.sharedSigs=Geteilte Signaturen
sign.noSavedSigs=Es wurden keine gespeicherten Signaturen gefunden sign.noSavedSigs=Es wurden keine gespeicherten Signaturen gefunden
sign.addToAll=Add to all pages sign.addToAll=Zu allen Seiten hinzufügen
sign.delete=Delete sign.delete=Löschen
sign.first=First page sign.first=Erste Seite
sign.last=Last page sign.last=Letzte Seite
sign.next=Next page sign.next=Nächste Seite
sign.previous=Previous page sign.previous=Vorherige Seite
#repair #repair
repair.title=Reparieren repair.title=Reparieren
@@ -949,17 +955,27 @@ multiTool.deleteSelected=Auswahl löschen
multiTool.downloadAll=Downloaden multiTool.downloadAll=Downloaden
multiTool.downloadSelected=Auswahl downloaden multiTool.downloadSelected=Auswahl downloaden
multiTool.insertPageBreak=Insert Page Break multiTool.insertPageBreak=Seitenumbruch einfügen
multiTool.addFile=Add File multiTool.addFile=Datei hinzufügen
multiTool.rotateLeft=Rotate Left multiTool.rotateLeft=Nach links drehen
multiTool.rotateRight=Rotate Right multiTool.rotateRight=Nach rechts drehen
multiTool.split=Split multiTool.split=Teilen
multiTool.moveLeft=Move Left multiTool.moveLeft=Nach links verschieben
multiTool.moveRight=Move Right multiTool.moveRight=Nach rechts verschieben
multiTool.delete=Delete multiTool.delete=Löschen
multiTool.dragDropMessage=Page(s) Selected multiTool.dragDropMessage=Ausgewählte Seite(n)
multiTool.undo=Undo multiTool.undo=Rückgängig machen
multiTool.redo=Redo multiTool.redo=Wiederherstellen
#decrypt
decrypt.passwordPrompt=Diese Datei ist passwortgeschützt. Bitte geben Sie das Passwort ein:
decrypt.cancelled=Vorgang für PDF abgebrochen: {0}
decrypt.noPassword=Kein Passwort für verschlüsseltes PDF angegeben: {0}
decrypt.invalidPassword=Bitte versuchen Sie es erneut mit dem richtigen Passwort.
decrypt.invalidPasswordHeader=Falsches Passwort oder nicht unterstützte Verschlüsselung für PDF: {0}
decrypt.unexpectedError=Bei der Verarbeitung der Datei ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut.
decrypt.serverError=Serverfehler beim Entschlüsseln: {0}
decrypt.success=Datei erfolgreich entschlüsselt.
#multiTool-advert #multiTool-advert
multiTool-advert.message=Diese Funktion ist auch auf unserer <a href="{0}">PDF-Multitool-Seite</a> verfügbar. Probieren Sie sie aus, denn sie bietet eine verbesserte Benutzeroberfläche und zusätzliche Funktionen! multiTool-advert.message=Diese Funktion ist auch auf unserer <a href="{0}">PDF-Multitool-Seite</a> verfügbar. Probieren Sie sie aus, denn sie bietet eine verbesserte Benutzeroberfläche und zusätzliche Funktionen!
@@ -1052,6 +1068,7 @@ addPassword.submit=Verschlüsseln
#watermark #watermark
watermark.title=Wasserzeichen hinzufügen watermark.title=Wasserzeichen hinzufügen
watermark.header=Wasserzeichen hinzufügen watermark.header=Wasserzeichen hinzufügen
watermark.customColor=Benutzerdefinierte Textfarbe
watermark.selectText.1=PDF auswählen, dem ein Wasserzeichen hinzugefügt werden soll: watermark.selectText.1=PDF auswählen, dem ein Wasserzeichen hinzugefügt werden soll:
watermark.selectText.2=Wasserzeichen Text: watermark.selectText.2=Wasserzeichen Text:
watermark.selectText.3=Schriftgröße: watermark.selectText.3=Schriftgröße:
@@ -1263,10 +1280,50 @@ splitByChapters.desc.3=Metadaten einschließen: Wenn diese Option aktiviert ist,
splitByChapters.desc.4=Duplikate erlauben: Wenn diese Option aktiviert ist, können mehrere Lesezeichen auf derselben Seite separate PDF Dateien erstellen. splitByChapters.desc.4=Duplikate erlauben: Wenn diese Option aktiviert ist, können mehrere Lesezeichen auf derselben Seite separate PDF Dateien erstellen.
splitByChapters.submit=PDF teilen splitByChapters.submit=PDF teilen
#File Chooser
fileChooser.click=Klicken
fileChooser.or=oder
fileChooser.dragAndDrop=Drag & Drop
fileChooser.hoveredDragAndDrop=Datei(en) hierhin Ziehen & Fallenlassen
#release notes #release notes
releases.footer=Releases releases.footer=Veröffentlichungen
releases.title=Release Notes releases.title=Versionshinweise
releases.header=Release Notes releases.header=Versionshinweise
releases.current.version=Current Release releases.current.version=Aktuelle Version
releases.note=Release notes are only available in English releases.note=Versionshinweise sind nur auf Englisch verfügbar
#Validate Signature
validateSignature.title=PDF-Signaturen überprüfen
validateSignature.header=Digitale Signaturen überprüfen
validateSignature.selectPDF=Signierte PDF-Datei auswählen
validateSignature.submit=Signaturen überprüfen
validateSignature.results=Gültigkeitsprüfungsergebnisse
validateSignature.status=Status
validateSignature.signer=Unterzeichner
validateSignature.date=Datum
validateSignature.reason=Grund
validateSignature.location=Ort
validateSignature.noSignatures=Keine digitalen Signaturen in diesem Dokument gefunden
validateSignature.status.valid=Gültig
validateSignature.status.invalid=Ungültig
validateSignature.chain.invalid=Zertifikatskettenprüfung fehlgeschlagen - kann die Identität des Unterzeichners nicht verifizieren
validateSignature.trust.invalid=Zertifikat nicht im Truststore - Quelle kann nicht verifiziert werden
validateSignature.cert.expired=Zertifikat ist abgelaufen
validateSignature.cert.revoked=Zertifikat wurde widerrufen
validateSignature.signature.info=Signaturinformationen
validateSignature.signature=Signatur
validateSignature.signature.mathValid=Signatur ist mathematisch gültig ABER:
validateSignature.selectCustomCert=Benutzerdefinierte Zertifikatsdatei X.509 (Optional)
validateSignature.cert.info=Zertifikat Details
validateSignature.cert.issuer=Aussteller
validateSignature.cert.subject=Betreff
validateSignature.cert.serialNumber=Seriennummer
validateSignature.cert.validFrom=Gültig von
validateSignature.cert.validUntil=Gültig bis
validateSignature.cert.algorithm=Algorithmus
validateSignature.cert.keySize=Schlüsselgröße
validateSignature.cert.version=Version
validateSignature.cert.keyUsage=Schlüsselverwendung
validateSignature.cert.selfSigned=Selbstsigniert
validateSignature.cert.bits=bits

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