Compare commits

...

533 Commits

Author SHA1 Message Date
Anthony Stirling
63eacf443e Merge pull request #610 from Frooodle/fix
required false
2023-12-30 02:20:34 +00:00
Anthony Stirling
b32c28e9cb Update build.gradle 2023-12-30 02:20:16 +00:00
Anthony Stirling
a5ee10e029 required flase 2023-12-30 02:19:30 +00:00
Anthony Stirling
bb1d41d74a Merge pull request #605 from Frooodle/expose-port
refactor: expose local application server port
2023-12-29 23:27:59 +00:00
Anthony Stirling
0698e2888d Merge pull request #608 from Frooodle/overlay
messages
2023-12-29 22:57:29 +00:00
Anthony Stirling
e1f0a6cb1d messages 2023-12-29 22:56:38 +00:00
Anthony Stirling
7fecae8b0d Merge pull request #607 from Frooodle/overlay
overlay fix for sequential
2023-12-29 22:54:07 +00:00
Anthony Stirling
e6622dfdc4 overlay fix for sequential 2023-12-29 22:53:46 +00:00
sbplat
34b4ae0e03 refactor: reflect changes to pipeline release 2023-12-29 17:13:36 -05:00
sbplat
036fd711f9 Merge branch 'main' into expose-port 2023-12-29 17:01:49 -05:00
Anthony Stirling
80a59205fa Merge pull request #570 from Frooodle/test
Pipeline release
2023-12-29 21:51:06 +00:00
Anthony Stirling
cbe4bca716 add banner and remove unused class 2023-12-29 21:46:17 +00:00
Anthony Stirling
232a556425 Merge branch 'test' of git@github.com:Frooodle/Stirling-PDF.git into test 2023-12-29 21:05:42 +00:00
Anthony Stirling
a497ad8c41 api changes to get metrics working 2023-12-29 21:05:32 +00:00
Anthony Stirling
3041e80c37 Merge branch 'main' into test 2023-12-29 20:48:27 +00:00
Anthony Stirling
1b2df20fdd reviews 2023-12-29 20:48:21 +00:00
Anthony Stirling
0e69f7e0e8 Merge pull request #604 from iLern/feat-zhCN
Enhance Chinese Translations and Correct Errors
2023-12-29 18:26:55 +00:00
sbplat
94aba370e0 refactor: expose local application server port 2023-12-29 13:05:01 -05:00
TieStone
cd49d7ffa2 add more Chinese Translations 2023-12-30 00:45:36 +08:00
TieStone
dc297644d1 add more Chinese Translations 2023-12-30 00:45:36 +08:00
TieStone
a7b4e44e6d remove duplicated property keys 2023-12-30 00:45:36 +08:00
Anthony Stirling
610ff22abe empty dir fix 2023-12-29 13:53:55 +00:00
Anthony Stirling
27e8335f79 Merge pull request #602 from Antiarchitect/set-file-encoding
Set java file.encoding to support non-latin customizations
2023-12-29 13:38:03 +00:00
Anthony Stirling
168a0f001c changes 2023-12-29 13:30:28 +00:00
Anthony Stirling
5c6936b494 Rework and cleanup 2023-12-29 12:55:22 +00:00
Anthony Stirling
a715dbb25d cleanup 2023-12-29 11:43:36 +00:00
Andrey Voronkov
a43e13cf94 Set java file.encoding to support non-latin customizations with UI_APPNAME, UI_HOMEDESCRIPTION, etc 2023-12-29 12:35:02 +03:00
Anthony Stirling
7f805d16a1 Merge pull request #600 from sudotman/main
Added missing Hindi (hi_IN) translations and fixed a few broken ones.
2023-12-29 07:57:43 +00:00
Satyam Kashyap
6f3cbe0cae added missing hindi (hi_IN) translations and fixed some broken translations - also updated readme to reflect the newly added translations 2023-12-29 06:05:48 +00:00
Anthony Stirling
44e3556382 Create CODEOWNERS 2023-12-28 23:13:47 +00:00
Anthony Stirling
c5ba546a02 Merge pull request #598 from vivekmaru36/add-hindi-language
Add hindi Translation
2023-12-28 18:56:27 +00:00
Anthony Stirling
d63519bbf4 Merge branch 'main' into add-hindi-language 2023-12-28 18:54:56 +00:00
Anthony Stirling
aeadc88f92 resolve .exe showing other entries it doesnt support 2023-12-28 18:46:54 +00:00
Anthony Stirling
829e98c29b MaxRAMPercentage 75 for #540 2023-12-28 17:49:45 +00:00
vivekmaru36
3e3f4a0188 Add hindi Translation 2023-12-28 23:09:38 +05:30
Anthony Stirling
e5bdd52b7c Merge pull request #589 from sbplat/main
fix: sequentially convert each pdf page into a BufferedImage to avoid getting out of memory errors for large pdf files
2023-12-28 17:39:02 +00:00
Anthony Stirling
e653ef6522 Merge branch 'main' into main 2023-12-28 17:32:38 +00:00
Anthony Stirling
5fcb4e893b pipeline refactor beginnings 2023-12-28 17:23:19 +00:00
Anthony Stirling
c2b524e459 Merge pull request #596 from eltociear/patch-2
chore: update README.md
2023-12-28 17:09:08 +00:00
sbplat
5a055bae5e Merge branch 'main' into main 2023-12-28 11:47:44 -05:00
Ikko Eltociear Ashimine
c3f501d701 chore: update README.md
a -> an
2023-12-29 00:34:56 +09:00
Anthony Stirling
8acab77ae3 contextPath fixes 2023-12-28 13:50:31 +00:00
Anthony Stirling
8bd2784f37 Merge pull request #594 from albanobattistella/patch-3
Update messages_it_IT.properties
2023-12-28 09:31:29 +00:00
albanobattistella
e2c5027311 Update messages_it_IT.properties 2023-12-28 10:28:21 +01:00
Anthony Stirling
d2b2adcbc1 Merge pull request #593 from aancw/id-translate
Add Indonesia Translation
2023-12-28 09:04:07 +00:00
Anthony Stirling
48158379ee Merge branch 'main' into id-translate 2023-12-28 09:03:02 +00:00
Aan
6ba84a190f translate file word to indonesia 2023-12-28 15:34:50 +07:00
Aan
d349aea1be Translate file word 2023-12-28 15:30:36 +07:00
Aan
79e2683cbe Keep watermark translation 2023-12-28 15:12:18 +07:00
Anthony Stirling
e4fb64ce16 Merge pull request #588 from Emad-Eldin-G/patch-1
Optimized the code in detect-blank-pages.py
2023-12-28 07:55:25 +00:00
sbplat
d405b7a810 perf: avoid re-rendering the first pdf page 2023-12-27 22:46:55 -05:00
sbplat
1d243a0ca5 fix: clean up redundant variable 2023-12-27 22:43:30 -05:00
sbplat
1f10693eaf fix: sequentially convert each pdf page into a BufferedImage to avoid getting MLE for large pdf files 2023-12-28 03:23:55 +00:00
EmadEldin Osman
b7f62a635d Optimized the code in detect-blank-pages.py
Made use of Numpy arrays
2023-12-28 04:51:50 +03:00
Anthony Stirling
4e991e7ec2 Merge pull request #582 from manuelkamp/patch-5
Update LocalRunGuide.md
2023-12-28 00:49:02 +00:00
Anthony Stirling
8fe7e57a6a Merge branch 'main' into patch-5 2023-12-28 00:47:06 +00:00
Anthony Stirling
5c79a5da29 Merge pull request #585 from Saneeitas/Saneeitas-patch-1
Update README.md
2023-12-28 00:37:24 +00:00
Muhammad Sani Ibrahim
d01473aceb Update README.md
made the english more standard
2023-12-28 01:27:25 +01:00
Anthony Stirling
3911be0177 Add Demo user 2023-12-27 22:56:51 +00:00
Anthony Stirling
78da44ad83 fix for #583 2023-12-27 19:28:49 +00:00
manuelkamp
54859ac3ba Update LocalRunGuide.md
Improved optional service section regarding environment variables in a separate .env file
2023-12-27 19:52:51 +01:00
Anthony Stirling
9cb8c9f655 Merge pull request #581 from manuelkamp/patch-4
Update LocalRunGuide.md
2023-12-27 18:48:30 +00:00
manuelkamp
dfda474ba5 Update LocalRunGuide.md
Added option to set up Stirling-PDF as a service
2023-12-27 19:30:28 +01:00
Anthony Stirling
43f15b3e55 Merge pull request #579 from manuelkamp/patch-3
Update LocalRunGuide.md
2023-12-27 18:10:13 +00:00
Anthony Stirling
86c45f6f8f Merge pull request #578 from manuelkamp/patch-2
Update LocalRunGuide.md
2023-12-27 18:10:03 +00:00
Anthony Stirling
de83321c62 Merge pull request #577 from manuelkamp/patch-1
Update LocalRunGuide.md #575
2023-12-27 18:09:43 +00:00
manuelkamp
7b44cf77d6 Update LocalRunGuide.md
Added "WeasyPrint" to pip3 install section
2023-12-27 18:54:55 +01:00
manuelkamp
c769a02982 Update LocalRunGuide.md
Edited run via java, since on previous step we moved the jar-file to /opt/Stirling-PDF
2023-12-27 18:51:51 +01:00
manuelkamp
aa671b8bd6 Update LocalRunGuide.md 2023-12-27 18:46:37 +01:00
Anthony Stirling
6e7c066e57 Merge pull request #574 from eltociear/patch-1
Update README.md
2023-12-27 15:39:54 +00:00
Ikko Eltociear Ashimine
78ac9231c5 Update README.md
temporay -> temporary
2023-12-28 00:34:30 +09:00
Anthony Stirling
e9947da5b4 changes 2023-12-27 15:18:26 +00:00
Anthony Stirling
8df7dfc3be Merge pull request #569 from albanobattistella/patch-2
Update messages_it_IT.properties
2023-12-27 15:03:44 +00:00
albanobattistella
d79db6f3da Update messages_it_IT.properties 2023-12-27 16:01:00 +01:00
Anthony Stirling
84aebe3851 validate button color 2023-12-27 13:22:28 +00:00
Anthony Stirling
f5c285a70f mild cleanup 2023-12-27 12:51:17 +00:00
Perdana Hadi
2d6bf43bdb update tags translations 2023-12-27 15:52:11 +07:00
Aan
964f22e3e0 Add Bahasa Indonesia 2023-12-27 14:43:32 +07:00
Aan
d325020e22 home page section done, todo tags extractImages.tags 2023-12-27 14:37:25 +07:00
Aan
aec85ddd66 missing quote 2023-12-27 14:09:57 +07:00
Perdana Hadi
32b009b11f remove extra line, add white space 2023-12-27 12:38:17 +07:00
Perdana Hadi
2e5b72e4fb add web-pages translation 2023-12-27 12:28:31 +07:00
Aan
1d3cf2bdc3 Remove DS_Store 2023-12-27 10:27:39 +07:00
Aan
2a5fe2bd74 27/12/2023 2023-12-27 10:06:09 +07:00
Anthony Stirling
61ff0248da close 2023-12-27 01:00:42 +00:00
Anthony Stirling
659af2089c setup 2023-12-27 00:53:31 +00:00
Anthony Stirling
8960313a2b Merge pull request #566 from sbplat/main
fix: add default value for the position in Add Page Numbers
2023-12-26 20:55:48 +00:00
Anthony Stirling
6ee8e1e37f auto disable in UI 2023-12-26 20:33:17 +00:00
Anthony Stirling
05977aa3a6 enableAlphaFunctionality 2023-12-26 20:10:37 +00:00
sbplat
f7dbb8d0a6 Merge branch 'main' of https://github.com/sbplat/Stirling-PDF 2023-12-26 13:32:13 -05:00
sbplat
eaf65d7981 fix: add selectedPosition style to the Add Page Number position selector default value 2023-12-26 13:31:33 -05:00
sbplat
a10e3a025b Merge branch 'Frooodle:main' into main 2023-12-26 13:27:21 -05:00
sbplat
4d3e442ecc fix: add default value for the position in Add Page Numbers 2023-12-26 13:26:37 -05:00
Anthony Stirling
49576c0aa4 Merge pull request #564 from farwill/farwill-patch-1
Fix README.md typo
2023-12-26 09:08:16 +00:00
farwill
960af83f11 Fix README.md typo 2023-12-26 14:30:18 +08:00
Aan
cf42ef7faa Intial Draft #1 2023-12-26 12:38:27 +07:00
Anthony Stirling
d894937c22 Merge pull request #560 from sbplat/main
fix(multitool): hide dragged pdf page at the start so it doesn't teleport
2023-12-25 22:27:30 +00:00
Anthony Stirling
8938e86223 Merge branch 'main' into main 2023-12-25 22:24:30 +00:00
Anthony Stirling
c1a39e53dc Merge pull request #559 from DimK10/main
Fixes #324 issue
2023-12-25 21:47:09 +00:00
dkaitantzidis
cf3693186a Fixes headers issue in merge pdfs. 2023-12-25 23:27:08 +02:00
Anthony Stirling
0fb0cb8bca apply local 2023-12-25 20:51:32 +00:00
dkaitantzidis
fb18d0d04d WIP: Fixes issue - needs refactor 2023-12-25 22:36:08 +02:00
sbplat
779d9028fe fix(multitool): hide dragged pdf page at the start so it doesn't teleport 2023-12-25 15:34:16 -05:00
Anthony Stirling
f2b701e3e3 todos 2023-12-25 18:52:11 +00:00
Anthony Stirling
a138d5f5a9 imports 2023-12-25 16:16:50 +00:00
Anthony Stirling
6276f028ac validate operations 2023-12-25 16:15:42 +00:00
Anthony Stirling
b962e867d8 log remove 2023-12-25 15:17:06 +00:00
Anthony Stirling
a286a92ede cleanups 2023-12-25 15:15:46 +00:00
Anthony Stirling
7fb8f5ed28 create logs dir 2023-12-25 15:03:45 +00:00
Anthony Stirling
03d3235e1d Merge remote-tracking branch 'origin/main' into test 2023-12-25 13:26:13 +00:00
Anthony Stirling
d23551857c deps 2023-12-25 13:00:44 +00:00
Anthony Stirling
dd9dd72f35 Role stuff 2023-12-25 12:58:49 +00:00
Anthony Stirling
9652f59ae9 Merge pull request #555 from sbplat/main
feat: merge pdf into multiple frames if the format is TIFF
2023-12-25 10:14:23 +00:00
sbplat
3469beb5b3 feat: merge pdf into multiple frames if the format is TIFF instead of a single big image 2023-12-24 22:52:39 -05:00
Anthony Stirling
690720f4e3 test interface 2023-12-25 01:25:17 +00:00
Anthony Stirling
491be75e1f Merge pull request #554 from sbplat/main
feat: make multiple images the default setting for PDF to Image
2023-12-24 22:29:07 +00:00
sbplat
a868b2c649 feat: make multiple images the default setting for PDF to Image 2023-12-24 16:55:11 -05:00
Anthony Stirling
0b49993d80 Merge pull request #553 from sbplat/main
fix!: map BMP and TIFF extensions to the proper MIME types
2023-12-24 21:50:17 +00:00
sbplat
995a926e35 fix!: map BMP and TIFF extensions to the proper MIME types
Remove the WBMP image format from PDF to Image
2023-12-24 15:27:16 -05:00
Anthony Stirling
914dd0a21a Merge pull request #552 from sbplat/main
fix: use JPEGFactory for jpeg
2023-12-24 18:02:12 +00:00
Anthony Stirling
d9b5d08b06 import clean 2023-12-24 17:56:31 +00:00
sbplat
344d1163ff Merge branch 'Frooodle:main' into main 2023-12-24 12:53:29 -05:00
sbplat
3f50979d3e fix: use JPEGFactory for jpeg 2023-12-24 12:52:55 -05:00
Anthony Stirling
c681f48459 Merge pull request #549 from sbplat/main
feat: add support for svg+webp images to convert to pdf
2023-12-24 17:18:37 +00:00
Anthony Stirling
2f5d7ed712 internal API plus brute force security 2023-12-24 17:12:32 +00:00
sbplat
1efefcfcb8 feat: add support for svg+webp images to convert to pdf 2023-12-23 20:27:04 -05:00
Anthony Stirling
909c9ed4d9 Merge pull request #548 from sbplat/main
Add remove annotations
2023-12-23 19:07:20 +00:00
sbplat
116b3535ee Update messages_en_GB.properties 2023-12-23 13:49:50 -05:00
sbplat
b7d6107a2d feat: add remove annotations 2023-12-23 13:47:21 -05:00
Anthony Stirling
120b017b1a serial 2023-12-23 16:26:09 +00:00
Anthony Stirling
24568f4a42 Merge pull request #547 from simovics/main
Adding Hungarian language support to Stirling-PDF
2023-12-23 16:00:54 +00:00
Anthony Stirling
03450454c5 pipeline 2023-12-23 15:47:18 +00:00
simovics
7e982e125d Create hu.svg
Adding Hungarian flag icon to the set.
2023-12-23 14:46:42 +01:00
simovics
e725451530 Update languages.html
Fixing indentation.
2023-12-23 14:21:51 +01:00
simovics
d7d6bc8108 Update languages.html
Adding Hungarian language reference to the navbar.
2023-12-23 14:19:07 +01:00
simovics
6d66ac0a8b Create messages_hu_HU.properties
Hungarian translation for Stirling-PDF.
2023-12-23 14:15:10 +01:00
Anthony Stirling
93f12d1313 pipeline changes 2023-12-23 12:29:32 +00:00
Anthony Stirling
eab9e3cffc changes pipeline 2023-12-20 19:29:13 +00:00
Anthony Stirling
d74c25e678 Merge pull request #542 from NeilJared/main
Update messages_es_ES.properties
2023-12-19 21:17:13 +00:00
NeilJared
c729b7201f Merge branch 'main' into main 2023-12-19 19:44:22 +01:00
NeilJared
beab9932d7 Update messages_es_ES.properties
Updated es_ES translation
2023-12-19 19:41:11 +01:00
Anthony Stirling
65fcf29fd5 Merge pull request #537 from Frooodle/dependabot/gradle/io.swagger.swaggerhub-1.3.2
Bump io.swagger.swaggerhub from 1.2.0 to 1.3.2
2023-12-19 18:34:24 +00:00
Anthony Stirling
73007239ee Merge branch 'main' into dependabot/gradle/io.swagger.swaggerhub-1.3.2 2023-12-19 18:32:46 +00:00
Anthony Stirling
816d874ac4 Merge pull request #541 from dhenry437/fix/bootstrap-icons
Fix for bootstrap icons
2023-12-19 13:59:05 +00:00
Dan Henry
b66f86f7cc add missing bootstrap icons link in head 2023-12-19 22:51:00 +11:00
Dan Henry
168ef747de add up to date minified bootstrap icons 2023-12-19 22:50:46 +11:00
dependabot[bot]
ad047ab012 Bump io.swagger.swaggerhub from 1.2.0 to 1.3.2
Bumps io.swagger.swaggerhub from 1.2.0 to 1.3.2.

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-18 22:45:51 +00:00
Anthony Stirling
1ea3fb209b ARG VERSION_TAG 2023-12-18 16:39:26 +00:00
Anthony Stirling
875d9da36b Update Dockerfile 2023-12-18 15:41:05 +00:00
Anthony Stirling
b21d2ecbd1 Update push-docker.yml 2023-12-18 15:38:37 +00:00
Anthony Stirling
3bffc1da76 gradle changes 2023-12-18 14:58:06 +00:00
Anthony Stirling
631d3948bd fix for #531 plus seperated out some things 2023-12-18 14:52:18 +00:00
Anthony Stirling
5774a22b64 possible fixes for overlay 2023-12-17 12:35:50 +00:00
ant
39345bb6bb Merge branch 'main' of git@github.com:Frooodle/Stirling-PDF.git into main 2023-12-17 12:31:24 +00:00
Anthony Stirling
57b483047e splitPdf sections cleanup 2023-12-17 12:23:11 +00:00
Anthony Stirling
0fb7633da8 Merge pull request #528 from NicolasFR/lang/FR_fr-updates
lang: update fr_FR
2023-12-17 11:34:09 +00:00
Nicolas
dae2f33772 lang: update fr_FR 2023-12-17 10:39:33 +01:00
Anthony Stirling
31ac877612 Update build.gradle 2023-12-16 19:56:56 +00:00
Anthony Stirling
79dcf99cce Clean imports and lang updates 2023-12-16 19:30:47 +00:00
Anthony Stirling
c28a40ffe8 Split by sections #506 2023-12-16 19:29:43 +00:00
Anthony Stirling
12dccab460 auth log #522 2023-12-16 18:18:00 +00:00
Anthony Stirling
0a26e2e6d6 new deps 2023-12-16 10:42:27 +00:00
Anthony Stirling
74f6cd63f4 other JS changes 2023-12-16 10:35:45 +00:00
Anthony Stirling
e8de5739fa Resolve has Update button to stay hidden if error 2023-12-16 10:26:35 +00:00
Anthony Stirling
1b2734d99c login enable and security stuff 2023-12-16 10:22:33 +00:00
Anthony Stirling
206cf40cb5 Update Dockerfile-ultra-lite
login
2023-12-16 10:11:34 +00:00
Anthony Stirling
78473e96fd Update Dockerfile-lite
fix for #526 Support for login in lite
2023-12-16 10:08:09 +00:00
Anthony Stirling
b7d6ac2cc3 extra fonts, ocr display full names, overlay fixes 2023-12-12 23:25:56 +00:00
Anthony Stirling
4068d9530f svg 2023-12-11 23:24:13 +00:00
Anthony Stirling
8a331956c2 Merge branch 'main' of git@github.com:Frooodle/Stirling-PDF.git into main 2023-12-11 23:20:43 +00:00
Anthony Stirling
1d3e018a56 init overlay and auto split 2023-12-11 23:20:31 +00:00
Anthony Stirling
3602034938 Merge pull request #520 from NicolasFR/patch-1
Update messages_fr_FR.properties
2023-12-11 18:39:05 +00:00
NicolasFR
eb4e2d5fca Update messages_fr_FR.properties 2023-12-11 17:56:38 +01:00
Anthony Stirling
b6671939e5 Merge pull request #517 from NeilJared/es_ES
Update messages_es_ES.properties
2023-12-11 12:12:17 +00:00
Anthony Stirling
41d09e40a1 Merge branch 'main' into es_ES 2023-12-11 12:11:10 +00:00
Anthony Stirling
9b0dba7f65 Update account.html
#515 fix
2023-12-11 12:10:10 +00:00
NeilJared
ac0dc8b5c7 Update messages_es_ES.properties
Updated es_ES translation
2023-12-11 13:06:59 +01:00
Anthony Stirling
723216c693 Merge pull request #516 from Frooodle/Frooodle-patch-1
Update build.gradle
2023-12-11 11:02:45 +00:00
Anthony Stirling
46f9a5057f Update build.gradle 2023-12-11 11:01:48 +00:00
Anthony Stirling
8a2633ca93 Update build.gradle 2023-12-11 10:59:05 +00:00
Anthony Stirling
a03470d2de Update Dockerfile 2023-12-11 10:09:28 +00:00
Anthony Stirling
ef7c98e5cb Merge pull request #514 from Frooodle/securityStuff
Security stuff
2023-12-11 10:00:57 +00:00
Anthony Stirling
c9cd1331d2 Merge branch 'main' into securityStuff 2023-12-11 09:58:09 +00:00
Anthony Stirling
ddc14517b8 Merge branch 'securityStuff' of git@github.com:Frooodle/Stirling-PDF.git into securityStuff 2023-12-11 09:57:37 +00:00
Anthony Stirling
578aecf977 WeasyPrint 2023-12-11 09:57:28 +00:00
Anthony Stirling
b1ca938053 Merge pull request #513 from Frooodle/securityStuff
fix
2023-12-11 09:35:23 +00:00
Anthony Stirling
298fe349c1 Merge branch 'main' into securityStuff 2023-12-11 09:33:20 +00:00
Anthony Stirling
f4364a3f33 bump 2023-12-11 09:33:05 +00:00
Anthony Stirling
e0f068bc9d test 2023-12-11 09:32:36 +00:00
Anthony Stirling
d7f8219b80 Update view-pdf.html 2023-12-10 23:22:25 +00:00
Anthony Stirling
87bc0fc975 Merge pull request #509 from Frooodle/securityStuff
Docker file security updates and minor fixes to things
2023-12-10 23:10:04 +00:00
Anthony Stirling
9f21ce96de Merge branch 'main' into securityStuff 2023-12-10 23:08:34 +00:00
Anthony Stirling
1f29033f17 docker changes 2023-12-10 23:06:35 +00:00
Anthony Stirling
59c7978330 docker and ocr updates 2023-12-10 22:02:30 +00:00
Anthony Stirling
8b55ffff96 changes 2023-12-10 16:33:44 +00:00
Anthony Stirling
a94808fd19 dark mode fixes 2023-12-10 15:29:12 +00:00
Anthony Stirling
7b2ffcff01 Merge pull request #504 from albanobattistella/patch-1
Update messages_it_IT.properties
2023-12-10 14:11:17 +00:00
Anthony Stirling
28a9daff62 init 2023-12-10 14:09:28 +00:00
albanobattistella
435753f50b Update messages_it_IT.properties 2023-12-08 16:01:14 +01:00
Anthony Stirling
1e6f288d72 Merge pull request #501 from alexdraconian/lang-ko
Add/improve Korean translation
2023-12-07 17:39:28 +00:00
Anthony Stirling
6d3fece5a6 Merge branch 'main' into lang-ko 2023-12-07 16:21:01 +00:00
alexdraconian
db926c50d8 Add/improve Korean translation 2023-12-07 21:47:20 +09:00
Anthony Stirling
dd9333f42e Merge pull request #491 from digitalindependent/patch-1
Typo fixed in HowToUseOCR.md
2023-12-02 23:39:42 +00:00
digitalindependent
3fa5acc51c Typo fixed in HowToUseOCR.md
ITS => IT'S
2023-12-02 22:34:43 +01:00
Anthony Stirling
15fa3df424 Merge pull request #475 from danielr1996/main
ci: add helm package/push to Jenkinsfile
2023-11-30 20:08:31 +00:00
Anthony Stirling
06c4ec95d5 Merge branch 'main' into main 2023-11-24 14:36:19 +00:00
Anthony Stirling
1a6afc1582 Merge pull request #470 from trytomakeyouprivate/main
added Podman and Distrobox notes
2023-11-24 13:35:10 +00:00
trytomakeyouprivate
ad2e1e4a18 Merge branch 'Frooodle:main' into main 2023-11-24 12:56:02 +00:00
Anthony Stirling
a1d0dcff41 Merge pull request #479 from Ludy87/german-translation
update language german
2023-11-17 15:16:57 +00:00
Ludy87
08da0f5c56 update language german 2023-11-17 16:07:02 +01:00
Daniel Richter
0b666674f7 fix(helm): move ci build to github actions
Signed-off-by: Daniel Richter <danielrichter@posteo.de>
2023-11-16 23:04:32 +01:00
Daniel Richter
d3fe467f6f ci: add helm package/push to Jenkinsfile
Signed-off-by: Daniel Richter <danielrichter@posteo.de>
2023-11-12 22:31:35 +01:00
trytomakeyouprivate
732fa0ec40 added Podman and Distrobox note 2023-11-07 11:49:11 +00:00
trytomakeyouprivate
7a7c978df2 added Podman
Podman is supposedly more Secure and used and distributed by Redhat and Fedora. It is CLI compatible with Docker
2023-11-07 11:45:42 +00:00
Anthony Stirling
9d052b310f Merge pull request #459 from Artem-ka-create/issue-372-pdfbox
Issue 372 pdfbox
2023-11-05 19:49:14 +00:00
Atrem Petrenko
8ff1a63276 Merge branch 'main' into issue-372-pdfbox 2023-11-05 14:49:04 +01:00
Anthony Stirling
ffd413ce7f Merge pull request #464 from NeilJared/main
Update messages_es_ES.properties
2023-11-05 11:23:18 +00:00
NeilJared
f2607bd161 Update messages_es_ES.properties
Modified!
2023-11-05 11:59:15 +01:00
NeilJared
ba5f3e12d7 Update messages_es_ES.properties
Updated Spanish translation
2023-11-05 11:39:53 +01:00
Anthony Stirling
ddc48429b1 Merge pull request #403 from Pixee-Bot-Java/pixeebot/drip-2023-10-05-pixee-java/secure-random
Introduced protections against predictable RNG abuse
2023-11-03 00:58:11 +00:00
Anthony Stirling
b8b7adbaf9 Merge pull request #456 from omerbustun/main
Add Turkish language support
2023-11-03 00:52:20 +00:00
Anthony Stirling
4ae945d08a Merge branch 'main' into main 2023-11-03 00:51:12 +00:00
Anthony Stirling
12f5a5e6d0 Merge pull request #460 from sanjeevneo/main
Enhance navbar for dark mode support
2023-11-02 19:16:48 +00:00
sanjeevneo
f85a7cb04d Enhance navbar for dark mode support 2023-11-03 02:15:02 +11:00
Atrem Petrenko
2f6a885bb0 update language properties 2023-11-02 12:13:33 +01:00
Atrem Petrenko
c8ac1f7029 implementing extracting tables from pdf by pdfbox 2023-11-02 11:50:50 +01:00
Atrem Petrenko
d6afb07533 starting issue, implementing pdf-to-csv view 2023-10-31 18:58:40 +01:00
Zach Carroll
a55f9f0ec8 Merge branch 'main' into pixeebot/drip-2023-10-05-pixee-java/secure-random 2023-10-30 15:26:14 -04:00
Ömer Üstün
06401d875b Merge branch 'Frooodle:main' into main 2023-10-30 20:01:26 +03:00
Ömer Üstün
4a29fd4b73 Update README.md 2023-10-30 19:59:15 +03:00
Ömer Üstün
02c53b90b3 Update messages_tr_TR.properties 2023-10-30 19:58:04 +03:00
Ömer Üstün
6ca1d82188 Update messages_tr_TR.properties 2023-10-30 19:19:12 +03:00
Anthony Stirling
18c5f5bb2b Merge pull request #455 from sanjeevneo/main
Optimize Dark Mode Toggle for Additional UI Elements
2023-10-30 10:34:27 +00:00
Anthony Stirling
33d21a7a85 Merge pull request #453 from tkymmm/main
Update messages_ja_JP.properties
2023-10-30 10:34:04 +00:00
Neo
c48c3e8897 Dark Mode Improvements 2023-10-30 18:29:55 +11:00
tkymmm
67f34016ce Update messages_ja_JP.properties 2023-10-30 09:21:02 +09:00
Ömer Üstün
c1434df259 Update messages_tr_TR.properties 2023-10-29 22:55:06 +03:00
Ömer Üstün
dd0eaf9182 Update messages_tr_TR.properties 2023-10-29 22:55:06 +03:00
Ömer Üstün
cfe50bcd81 Update messages_tr_TR.properties 2023-10-29 22:55:06 +03:00
omerbustun
a75bbff7cf Update messages_tr_TR.properties 2023-10-29 22:55:06 +03:00
omerbustun
2e9d88da0e Add Turkish option to navbar language selector
- Updated the navbar to include the Turkish language option.
- Added the Turkish flag SVG to the assets folder.
- Translated a portion of the .properties file to support Turkish.
2023-10-29 22:55:06 +03:00
Anthony Stirling
124c7801c5 Merge pull request #440 from quyleanh/patch-1
Update README.md to correct Endpoint-groups.md link
2023-10-29 15:59:24 +00:00
Anthony Stirling
8490613ada Merge pull request #450 from maxi322/main
Add german translation for View PDF
2023-10-29 15:59:12 +00:00
Anthony Stirling
80553ce95a Merge branch 'main' into patch-1 2023-10-28 15:29:22 +01:00
maxi322
d9206bfd2a Add german translation for View PDF 2023-10-28 15:31:44 +02:00
Anthony Stirling
7aae688db2 add searchbar translation option 2023-10-28 13:30:48 +01:00
Anthony Stirling
347b4cfa85 Update build.gradle 2023-10-28 12:14:11 +01:00
Anthony Stirling
f2eebcc396 extra lang 2023-10-28 12:12:54 +01:00
Anthony Stirling
19c26f0552 lang update 2023-10-28 12:06:23 +01:00
Anthony Stirling
d532db91f9 fixes for #438 and #423 2023-10-28 10:40:26 +01:00
Anthony Stirling
bd0bf404f5 Merge pull request #445 from DimK10/Feature-request-pdf-reader
Adds navbar logo to pdf viewer
2023-10-27 18:28:33 +01:00
Dimitris Kaitantzidis
e51a9c209a Merge branch 'main' into Feature-request-pdf-reader 2023-10-27 20:11:14 +03:00
Dimitrios Kaitantzidis
6bf172fb25 Adds navbar logo to pdf viewer 2023-10-27 20:08:13 +03:00
Anthony Stirling
a1e93e0f5d Update .gitattributes 2023-10-25 22:07:05 +01:00
Anthony Stirling
6392f6ec12 Merge pull request #442 from joleaf/main
Fix help text in addPageNumbers.customNumberDesc
2023-10-25 10:20:33 +01:00
joleaf
fbdff5c97f Update messages_it_IT.properties - fix helptext addPageNumbers.customNumberDesc 2023-10-25 10:55:13 +02:00
joleaf
2ecc4ed080 Update messages_es_ES.properties - Fix helptext addPageNumbers.customNumberDesc 2023-10-25 10:54:21 +02:00
joleaf
3318cb96b2 Update messages_de_DE.properties - fix helptext addPageNumbers.customNumberDesc 2023-10-25 10:53:10 +02:00
Quy Le Anh
6be0a1fb05 Update README.md to correct Endpoint-groups.md link 2023-10-24 17:11:48 +09:00
Anthony Stirling
ab1297aee0 Version bump and readme change 2023-10-22 23:14:18 +01:00
Anthony Stirling
25a0cb7681 Merge pull request #439 from DimK10/Feature-request-pdf-reader
Feature request pdf reader
2023-10-22 23:02:14 +01:00
Dimitrios Kaitantzidis
116b034878 Adds support for greek language. 2023-10-23 00:56:53 +03:00
Dimitrios Kaitantzidis
038de2e264 Adds ignore for .idea folder entirely 2023-10-23 00:43:43 +03:00
Dimitrios Kaitantzidis
7e51cf8c5a Removes .idea files and updates README.md. 2023-10-23 00:40:03 +03:00
Dimitrios Kaitantzidis
a1eadba769 Fixes .idea ignored files. 2023-10-23 00:27:36 +03:00
Dimitrios Kaitantzidis
3145f5fdd0 Feature ready. 2023-10-23 00:12:13 +03:00
Dimitrios Kaitantzidis
8393dd4731 Deletes unnecessary files 2023-10-22 20:54:23 +03:00
Dimitrios Kaitantzidis
768877d969 Reader works correctly 2023-10-22 20:32:19 +03:00
Dimitrios Kaitantzidis
14a90f5e50 WIP: No errors but nothing is working 2023-10-22 20:16:59 +03:00
Dimitrios Kaitantzidis
99b0150e7a WIP: Adds Drap and drop 2023-10-21 11:26:58 +03:00
Anthony Stirling
3be12c8988 Merge pull request #424 from DimK10/Bug-Multitool-Filename
Bug multitool filename
2023-10-15 17:37:46 +01:00
Dimitrios Kaitantzidis
89345c8d60 Removes commas in multiple dots with text. 2023-10-15 19:10:30 +03:00
Dimitrios Kaitantzidis
49f1f4e7c7 Fixes bug with unfinished filename extension (multiple dots problem) 2023-10-15 19:03:10 +03:00
Dimitrios Kaitantzidis
d0ce7db9ee Fixes bug with unfinished filename extension (file.p) 2023-10-15 18:35:39 +03:00
Dimitrios Kaitantzidis
53a0291cc2 Adds requested changes 2023-10-15 16:49:12 +03:00
Dimitrios Kaitantzidis
9a3bc839dd Merge remote-tracking branch 'origin/Bug-Multitool-Filename' into Bug-Multitool-Filename 2023-10-14 00:38:16 +03:00
Dimitrios Kaitantzidis
e519840bd6 Fixes issue with light theme support. 2023-10-14 00:37:52 +03:00
Dimitris Kaitantzidis
ed32a3ca33 Merge branch 'main' into Bug-Multitool-Filename 2023-10-14 00:06:36 +03:00
Dimitrios Kaitantzidis
369ac99a16 Fixes issue. 2023-10-14 00:03:08 +03:00
Anthony Stirling
39ac823b05 Merge pull request #308 from Frooodle/dependabot/gradle/org.springdoc-springdoc-openapi-starter-webmvc-ui-2.2.0
Bump org.springdoc:springdoc-openapi-starter-webmvc-ui from 2.1.0 to 2.2.0
2023-10-11 21:05:58 +01:00
Anthony Stirling
548ae4dba3 Merge branch 'main' into dependabot/gradle/org.springdoc-springdoc-openapi-starter-webmvc-ui-2.2.0 2023-10-11 21:04:36 +01:00
Anthony Stirling
f69d593649 Merge pull request #309 from Frooodle/dependabot/gradle/com.google.zxing-core-3.5.2
Bump com.google.zxing:core from 3.5.1 to 3.5.2
2023-10-11 21:04:27 +01:00
Anthony Stirling
423af5f077 Merge branch 'main' into dependabot/gradle/com.google.zxing-core-3.5.2 2023-10-11 15:40:21 +01:00
Anthony Stirling
76e5e1ad00 Merge pull request #311 from Frooodle/dependabot/gradle/io.spring.dependency-management-1.1.3
Bump io.spring.dependency-management from 1.1.2 to 1.1.3
2023-10-11 14:49:03 +01:00
Anthony Stirling
420caa4d8d Merge branch 'main' into dependabot/gradle/io.spring.dependency-management-1.1.3 2023-10-11 14:45:02 +01:00
Anthony Stirling
fdeeb68a6d Merge pull request #367 from Frooodle/dependabot/gradle/edu.sc.seis.launch4j-3.0.5
Bump edu.sc.seis.launch4j from 3.0.3 to 3.0.5
2023-10-11 14:44:48 +01:00
Anthony Stirling
9fe803a0b1 Merge branch 'main' into dependabot/gradle/edu.sc.seis.launch4j-3.0.5 2023-10-11 14:43:45 +01:00
Anthony Stirling
dd9ba90a03 Merge pull request #379 from Frooodle/dependabot/gradle/org.springframework.boot-spring-boot-starter-test-3.1.4
Bump org.springframework.boot:spring-boot-starter-test from 3.1.2 to 3.1.4
2023-10-11 14:43:41 +01:00
Anthony Stirling
2bc739316a Merge branch 'main' into dependabot/gradle/org.springframework.boot-spring-boot-starter-test-3.1.4 2023-10-11 14:42:04 +01:00
Dimitrios Kaitantzidis
74da8c340d Adds support for disabled filename input if not pdf is loaded. Need to disable the input if all pages are deleted. 2023-10-10 20:24:11 +03:00
Anthony Stirling
766cb4410b Update pull_request_template.md 2023-10-08 19:07:08 +01:00
Anthony Stirling
78fe6d6ea8 Create pull_request_template.md 2023-10-08 19:06:08 +01:00
Anthony Stirling
6b99decb56 Create pull_request_template.md 2023-10-08 19:04:32 +01:00
Dimitrios Kaitantzidis
323745e61f Change in name works. 2023-10-08 19:57:19 +03:00
Anthony Stirling
22c19670e9 Merge pull request #415 from DimK10/Feature_Request-Bigger_text_input
Feature request bigger text input
2023-10-08 17:06:32 +01:00
Dimitrios Kaitantzidis
a1b7aaddb8 Changes filename using js, but the old filename persists in download 2023-10-08 18:59:43 +03:00
Dimitris Kaitantzidis
cfba9681c4 Merge branch 'main' into Feature_Request-Bigger_text_input 2023-10-08 17:13:43 +03:00
Dimitrios Kaitantzidis
a019b8b5ca Deletes console.log statements and commented out code. 2023-10-08 17:02:03 +03:00
Dimitrios Kaitantzidis
bd9b267562 Fixes minor typos 2023-10-08 16:04:35 +03:00
Dimitrios Kaitantzidis
b0f8f56650 WIP: Letters are now inside canvas, and multiline support is functional. There is a small bug where when there are a lot of multiple lines, the font gets smaller. 2023-10-08 15:58:23 +03:00
Dimitrios Kaitantzidis
a71e813e82 WIP: Makes font smaller but test is shown as whole and in multiline 2023-10-08 15:51:40 +03:00
Anthony Stirling
75e7665d6e Merge pull request #353 from demonisius/ru_translation_update
Update messages_ru_RU.properties
2023-10-08 00:09:33 +01:00
Anthony Stirling
12293f5297 Merge branch 'main' into ru_translation_update 2023-10-08 00:08:27 +01:00
Anthony Stirling
4899ee0bee Merge pull request #409 from woodchen-ink/patch-1
Update messages_zh_CN.properties
2023-10-08 00:06:40 +01:00
Anthony Stirling
563c17c84e Merge branch 'main' into patch-1 2023-10-08 00:05:36 +01:00
Anthony Stirling
d94eca4ee7 blank == null 2023-10-07 23:35:28 +01:00
Anthony Stirling
f4a01884bd test empty = null for settings 2023-10-07 23:23:56 +01:00
Anthony Stirling
105d7f12ac Merge pull request #412 from Craftplorer/patch-1
Update messages_de_DE.properties
2023-10-07 21:39:35 +01:00
Jonas Meyer
30c115a7de Update messages_de_DE.properties 2023-10-07 22:10:35 +02:00
Dimitrios Kaitantzidis
88a90f22a3 WIP: Adds textarea and multi line support for pdf sign. 2023-10-07 22:54:11 +03:00
Anthony Stirling
c72c712c1b Merge pull request #411 from DimK10/greek_language
Adds Greek language support
2023-10-07 19:06:16 +01:00
Dimitrios Kaitantzidis
7205801e76 Fixes some typos 2023-10-07 20:46:58 +03:00
Dimitrios Kaitantzidis
64f4f54b9d Adds greek language support 2023-10-07 20:39:30 +03:00
Dimitrios Kaitantzidis
0287d88895 WIP: Adds greek language support 2023-10-07 20:25:39 +03:00
Dimitrios Kaitantzidis
cdc075b27c WIP: Adds greek language support 2023-10-07 18:56:43 +03:00
Dimitrios Kaitantzidis
604d9827c5 WIP: Adds greek language support 2023-10-07 18:39:37 +03:00
wood chen
bdeb6bf188 Update messages_zh_CN.properties 2023-10-07 22:58:34 +08:00
Dimitrios Kaitantzidis
19e122be99 WIP: Adds greek language support 2023-10-07 17:53:03 +03:00
Anthony Stirling
f3ddf18a23 Update README.md 2023-10-05 23:53:40 +01:00
pixeebot[bot]
db488b39bb Introduced protections against predictable RNG abuse 2023-10-05 20:29:56 +00:00
Anthony Stirling
51f863e1e4 swagger docs fix #401 2023-10-05 21:20:05 +01:00
Anthony Stirling
0a9381d538 Update pdf-to-img.html 2023-10-05 18:45:33 +01:00
dependabot[bot]
a9514b54eb Bump edu.sc.seis.launch4j from 3.0.3 to 3.0.5
Bumps edu.sc.seis.launch4j from 3.0.3 to 3.0.5.

---
updated-dependencies:
- dependency-name: edu.sc.seis.launch4j
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-03 21:58:50 +00:00
dependabot[bot]
d647cb196f Bump org.springframework.boot:spring-boot-starter-test
Bumps [org.springframework.boot:spring-boot-starter-test](https://github.com/spring-projects/spring-boot) from 3.1.2 to 3.1.4.
- [Release notes](https://github.com/spring-projects/spring-boot/releases)
- [Commits](https://github.com/spring-projects/spring-boot/compare/v3.1.2...v3.1.4)

---
updated-dependencies:
- dependency-name: org.springframework.boot:spring-boot-starter-test
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-03 21:58:50 +00:00
Anthony Stirling
b69973f614 Split doc size fix #400 2023-10-03 22:58:15 +01:00
Anthony Stirling
fb9c42f4a1 Merge pull request #398 from deraw/update-fr-translations
Update messages_fr_FR.properties
2023-10-03 00:53:08 +01:00
Dylan Broussard
b873e3cdf8 Update messages_fr_FR.properties 2023-10-02 23:54:01 +02:00
Anthony Stirling
e9fc024332 Merge pull request #392 from nimdassdev/main
BG lang support
2023-10-01 15:17:27 +01:00
IT Creativity + Art Team
d4956fad8c bg lang flag
- bg lang flag
2023-10-01 17:05:42 +03:00
IT Creativity + Art Team
9d1dfe742e Update languages.html
Added BG lang support
2023-10-01 15:59:57 +03:00
IT Creativity + Art Team
e32c092af8 BG lang support
Hello Team,

This is the Bulgarian Translation for Stirling-PDF.

Thank you.
2023-10-01 15:56:22 +03:00
Anthony Stirling
18a2664b54 Merge pull request #373 from jr-instantsystem/features/335_helm_chart
Add helm chart
2023-09-30 10:29:23 +01:00
Anthony Stirling
954e46c5ec Merge branch 'main' into features/335_helm_chart 2023-09-30 10:27:38 +01:00
Anthony Stirling
e0f306d3f7 updates 2023-09-29 23:58:37 +01:00
Anthony Stirling
09db6618d6 Merge pull request #390 from JerronAB/main
Removed numpy library from detect-blank-pages.py
2023-09-29 21:24:29 +01:00
JerronAB
c5ea254945 Removed numpy library from blank page py script 2023-09-29 16:00:22 -04:00
Anthony Stirling
1fc1ecbaa6 Update remove-password.html
allow removal of encryption without password #383
2023-09-28 12:35:44 -05:00
Anthony Stirling
bc4640c3f0 Merge branch 'main' into features/335_helm_chart 2023-09-27 10:51:33 -05:00
Anthony Stirling
1e2eb9b07a additional fix to #364 2023-09-24 21:21:01 +01:00
Anthony Stirling
ece00956d9 fix for config #376 2023-09-24 21:09:34 +01:00
Anthony Stirling
af5bbd8838 Merge pull request #375 from slikie/patch-2
Docs Fix - Ensure proper context path format
2023-09-20 09:21:07 -05:00
slikie
3a8f2495ea fix: Docs - Ensure proper context path format 2023-09-20 22:12:47 +08:00
slikie
993f5e5097 Update README.md
spelling
2023-09-20 22:08:31 +08:00
Julien Rouvier
1f99c26e78 Add helm chart 2023-09-20 10:07:11 +02:00
Anthony Stirling
05ebf3a6b4 Merge pull request #366 from Frooodle/image
image to pdf change and cert atempt fix
2023-09-17 21:21:19 +01:00
Anthony Stirling
1be3046d26 version bump 2023-09-17 21:17:47 +01:00
Anthony Stirling
5b3858ba29 image changes and cert fix 2023-09-17 21:17:13 +01:00
Anthony Stirling
a1f388e524 Merge pull request #365 from NeilJared/main
Update messages_es_ES.properties
2023-09-17 08:04:52 +01:00
NeilJared
cf14ff1540 Update messages_es_ES.properties
Updated es_ES translation including changes of the latest version
2023-09-16 23:39:12 +02:00
Anthony Stirling
a0ac2bc02a Update build.gradle 2023-09-14 21:25:18 +01:00
Anthony Stirling
ed82c492ab Update build.gradle 2023-09-14 21:08:26 +01:00
Anthony Stirling
fc2d71d120 Update Dockerfile remove PUID 2023-09-14 21:07:39 +01:00
Anthony Stirling
e5a7a0631b Merge pull request #360 from Frooodle/itextRemoval
Itext removal password enhancement and others
2023-09-14 14:36:10 +01:00
Anthony Stirling
315cba07f0 Merge branch 'main' into itextRemoval 2023-09-14 14:34:14 +01:00
Anthony Stirling
cd5bd92a41 docs 2023-09-14 14:30:39 +01:00
Anthony Stirling
10126ce979 readme 2023-09-14 13:35:33 +01:00
Anthony Stirling
f6c4f08254 docs 2023-09-14 13:32:49 +01:00
Anthony Stirling
fc4feb2096 lang 2023-09-14 11:33:02 +01:00
Anthony Stirling
82b641458f fixes 2023-09-13 00:46:30 +01:00
Anthony Stirling
2c0fb33548 slow rework testing, still on multipage 2023-09-12 00:42:22 +01:00
Anthony Stirling
24e665bfd5 cleanup and defaults 2023-09-11 23:42:18 +01:00
Anthony Stirling
1404e33386 Merge branch 'itextRemoval' of git@github.com:Frooodle/Stirling-PDF.git
into itextRemoval
2023-09-11 23:21:05 +01:00
Anthony Stirling
0b1fd61188 api /api/v1/ 2023-09-11 23:19:50 +01:00
Anthony Stirling
80b4c67e35 super call 2023-09-10 09:24:20 +01:00
Anthony Stirling
1f4aae9249 finishing forms 2023-09-10 01:10:24 +01:00
Anthony Stirling
145e8006f0 metadata 2023-09-09 18:51:43 +01:00
Anthony Stirling
872f562aad misc beginings 2023-09-09 18:21:55 +01:00
Anthony Stirling
db70d67180 Merge branch 'itextRemoval' of git@github.com:Frooodle/Stirling-PDF.git into itextRemoval 2023-09-09 00:25:47 +01:00
Anthony Stirling
b1fd798a5c init 2023-09-09 00:25:27 +01:00
Дмитрий
42907ade21 Update messages_ru_RU.properties
Added translation
2023-09-08 11:34:03 +03:00
dependabot[bot]
b3bc0b4e5a Bump org.springdoc:springdoc-openapi-starter-webmvc-ui
Bumps [org.springdoc:springdoc-openapi-starter-webmvc-ui](https://github.com/springdoc/springdoc-openapi) from 2.1.0 to 2.2.0.
- [Release notes](https://github.com/springdoc/springdoc-openapi/releases)
- [Changelog](https://github.com/springdoc/springdoc-openapi/blob/main/CHANGELOG.md)
- [Commits](https://github.com/springdoc/springdoc-openapi/compare/v2.1.0...v2.2.0)

---
updated-dependencies:
- dependency-name: org.springdoc:springdoc-openapi-starter-webmvc-ui
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-06 21:24:15 +00:00
dependabot[bot]
12e24f3ec1 Bump com.google.zxing:core from 3.5.1 to 3.5.2
Bumps [com.google.zxing:core](https://github.com/zxing/zxing) from 3.5.1 to 3.5.2.
- [Release notes](https://github.com/zxing/zxing/releases)
- [Changelog](https://github.com/zxing/zxing/blob/master/CHANGES)
- [Commits](https://github.com/zxing/zxing/compare/zxing-3.5.1...zxing-3.5.2)

---
updated-dependencies:
- dependency-name: com.google.zxing:core
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-06 21:24:15 +00:00
Anthony Stirling
fefa8347da Merge pull request #342 from Frooodle/itextRemoval
Itext removal
2023-09-06 22:23:29 +01:00
Anthony Stirling
cd7ca09a3f Update settings.yml.template 2023-09-06 22:22:13 +01:00
Anthony Stirling
3960b2d6f9 Merge branch 'main' into itextRemoval 2023-09-06 22:10:41 +01:00
Anthony Stirling
264969e80c Merge branch 'itextRemoval' of git@github.com:Frooodle/Stirling-PDF.git into itextRemoval 2023-09-06 21:58:36 +01:00
Anthony Stirling
4e2911f648 custom font fix 2023-09-06 21:58:28 +01:00
Anthony Stirling
42610b2645 Merge branch 'main' into dependabot/gradle/io.spring.dependency-management-1.1.3 2023-09-06 18:38:53 +01:00
Anthony Stirling
7b07a706e1 Merge pull request #350 from Ale1x/main
Update content in messages_it.properties file
2023-09-06 16:35:49 +01:00
Anthony Stirling
8fef9dc8a8 Merge branch 'main' into itextRemoval 2023-09-06 15:41:39 +01:00
Anthony Stirling
83936bf4c8 security fix 2023-09-06 15:41:15 +01:00
Alessandro Passarelli
65f89e283e Fixed Mistakes 2023-09-06 15:57:43 +02:00
Alessandro Passarelli
1aa65bd3d1 Update content in messages_it.properties file
This commit includes changes to the content of the messages_it.properties file. Modifications were made to enhance the clarity and accuracy of the messages for the Italian localization.
2023-09-06 15:45:20 +02:00
Anthony Stirling
0547ec3c49 Merge pull request #349 from rubenixnagios/rubenixnagios-patch-2
Update messages_ca_CA.properties
2023-09-06 12:32:49 +01:00
Rubens
7e1cbe572d Update messages_ca_CA.properties
updated catalan missing translations
2023-09-06 13:13:20 +02:00
Anthony Stirling
7a98f30d05 Merge pull request #347 from darddan/main
Add side stitch booklet rearrangement
2023-09-05 21:44:11 +01:00
Anthony Stirling
c565bd4400 test 2023-09-05 20:58:18 +01:00
Dardan
d039c8e62e Use tabs instead of spaces 2023-09-05 20:06:18 +02:00
Dardan
da7f0561cb Add side stitch booklet rearrangement 2023-09-05 19:48:16 +02:00
Anthony Stirling
00210b75c7 Merge pull request #344 from c-jaenicke/main
translate text to german
2023-09-05 11:09:44 +01:00
c-jaenicke
3fe8973ae6 Merge branch 'main' into main 2023-09-05 11:36:36 +02:00
Anthony Stirling
87c4b42683 Merge pull request #346 from tkymmm/tkymmm-patch-1
Update messages_ja_JP.properties
2023-09-05 09:42:00 +01:00
tkymmm
0901eaac45 Update messages_ja_JP.properties
Japanese translation of new features.
2023-09-05 14:58:31 +09:00
Anthony Stirling
c7c81a7243 security 2023-09-04 18:42:22 +01:00
Anthony Stirling
ac019ac196 Merge remote-tracking branch 'origin/main' into itextRemoval 2023-09-04 18:30:30 +01:00
Anthony Stirling
b12c1305ea Merge pull request #345 from ikerib/basque_locale
[Locale] [Basque] Add new strings
2023-09-04 18:00:51 +01:00
ikerib
1876c2afae Add new strings 2023-09-04 17:58:03 +02:00
c-jaenicke
4575375ea9 fix line 1 comment 2023-09-04 12:31:56 +02:00
c-jaenicke
aac7836dce spellchecking and last translation 2023-09-04 12:16:42 +02:00
c-jaenicke
df5de2d60d make format consistent 2023-09-04 11:47:18 +02:00
c-jaenicke
b026283ef2 translate text to german 2023-09-04 11:45:00 +02:00
c-jaenicke
548e5d108c translate text to german 2023-09-04 11:32:15 +02:00
c-jaenicke
4d31152a02 translate text to german 2023-09-04 11:08:05 +02:00
Anthony Stirling
fd08513212 dipslay stuf 2023-09-04 00:12:27 +01:00
Anthony Stirling
3175ac16d1 Merge pull request #343 from deraw/update-fr-translations
Update messages_fr_FR.properties
2023-09-04 00:00:30 +01:00
Dylan Broussard
0cb5a6c7ad Update messages_fr_FR.properties 2023-09-04 00:48:08 +02:00
Anthony Stirling
0bb2df135b testing messages 2023-09-03 19:44:16 +01:00
Anthony Stirling
adadf7428c Merge branch 'itextRemoval' of git@github.com:Frooodle/Stirling-PDF.git into itextRemoval 2023-09-03 16:41:00 +01:00
Anthony Stirling
146dd3c00b cred change start 2023-09-03 16:40:40 +01:00
Anthony Stirling
78bfa84afd Update README.md 2023-09-03 01:46:02 +01:00
Anthony Stirling
07512c7e2c Merge branch 'main' into itextRemoval 2023-09-03 01:24:49 +01:00
Anthony Stirling
9fe96bec40 import cleanup 2023-09-03 01:24:02 +01:00
Anthony Stirling
18172aa33a complete itext removal 2023-09-03 01:23:44 +01:00
Anthony Stirling
9ece6dacbd removal 2023-09-02 20:32:44 +01:00
Anthony Stirling
ef07963d79 itext removal 2023-09-02 20:21:55 +01:00
Anthony Stirling
862086eae5 itext removal fixes 2023-09-02 19:12:08 +01:00
Anthony Stirling
277aa0c7e1 Merge pull request #341 from shalak/tune_docs
Update settings docs
2023-09-02 12:13:15 +01:00
Shalak
fc52741435 Update settings docs 2023-09-02 12:30:27 +02:00
Anthony Stirling
a7cd6bfd2e itext changes 2023-09-02 00:05:50 +01:00
Anthony Stirling
648df7b3d3 Update README.md 2023-08-31 21:24:53 +01:00
Anthony Stirling
01f7f1f59c fix max size issue 2023-08-31 13:52:54 +01:00
Anthony Stirling
ff7f089f69 v tag fix 2023-08-30 22:55:33 +01:00
dependabot[bot]
4e06e8c0c0 Bump io.spring.dependency-management from 1.1.2 to 1.1.3
Bumps [io.spring.dependency-management](https://github.com/spring-gradle-plugins/dependency-management-plugin) from 1.1.2 to 1.1.3.
- [Release notes](https://github.com/spring-gradle-plugins/dependency-management-plugin/releases)
- [Commits](https://github.com/spring-gradle-plugins/dependency-management-plugin/compare/v1.1.2...v1.1.3)

---
updated-dependencies:
- dependency-name: io.spring.dependency-management
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-30 21:53:43 +00:00
Anthony Stirling
ca5ce905c7 version bump 2023-08-30 22:52:53 +01:00
Anthony Stirling
0fc29de02c redact allow colors, bug fixes 2023-08-30 22:52:38 +01:00
Anthony Stirling
8509a16d6e fix init for multi downloads 2023-08-30 21:40:29 +01:00
Anthony Stirling
0ed021357b change metadata fix 2023-08-30 18:52:31 +01:00
Anthony Stirling
58ad7a1e8a info permission fix 2023-08-30 00:12:40 +01:00
Anthony Stirling
622ee29dd8 Update README.md 2023-08-28 13:01:57 +01:00
Anthony Stirling
5cbb4c6223 Update README.md 2023-08-28 13:01:16 +01:00
Anthony Stirling
5299859385 Update README.md 2023-08-28 12:49:23 +01:00
Anthony Stirling
c57b308909 Merge pull request #327 from Frooodle/loginPage
Login page tweaks
2023-08-28 10:19:23 +01:00
Anthony Stirling
6409274f83 language stuff 2023-08-28 10:17:02 +01:00
Anthony Stirling
bc534c12a5 login page lang changes 2023-08-28 00:49:14 +01:00
Anthony Stirling
b58fd2022a docker version stuff 2023-08-27 19:58:20 +01:00
Anthony Stirling
d850d026ed fixes 2023-08-27 17:02:12 +01:00
Anthony Stirling
83627686d4 Update releaseArtifacts.yml 2023-08-27 14:41:02 +01:00
Anthony Stirling
4e7d01c72c Update releaseArtifacts.yml 2023-08-27 13:52:26 +01:00
Anthony Stirling
0f8ab20db7 Merge pull request #325 from Frooodle/testversions
test dynamic security
2023-08-27 13:47:19 +01:00
Anthony Stirling
88cc90786d testing github actions 2023-08-27 13:45:58 +01:00
Anthony Stirling
fb66717b43 docker file testing 2023-08-27 13:45:37 +01:00
Anthony Stirling
1d60433fcf test 2023-08-27 11:59:08 +01:00
Anthony Stirling
0f3df6e92b cleanup imports 2023-08-27 00:39:22 +01:00
Anthony Stirling
ca7c63c7d7 name changes 2023-08-27 00:38:17 +01:00
Anthony Stirling
135f9611df hometext fix 2023-08-26 23:45:43 +01:00
Anthony Stirling
cfaaeebd4a test 2023-08-26 23:33:35 +01:00
Anthony Stirling
09a0779180 Merge pull request #322 from Frooodle/bootstrap5_test
Bootstrap5
2023-08-26 22:35:59 +01:00
Anthony Stirling
7f7d09bc85 Update README.md 2023-08-26 22:35:10 +01:00
Anthony Stirling
0c454a08dc rename to settins.yml 2023-08-26 22:33:23 +01:00
Anthony Stirling
d749b63549 configs and app setup stuff 2023-08-26 17:30:49 +01:00
Anthony Stirling
2053a6950d config template 2023-08-26 12:27:52 +01:00
Anthony Stirling
41bd801e0d redact stuff, login page, lang and remember me 2023-08-25 23:39:18 +01:00
Anthony Stirling
cd0e1a3962 redact 2023-08-24 23:23:25 +01:00
Anthony Stirling
7c2f482b3b minor changes 2023-08-22 23:44:38 +01:00
Anthony Stirling
7741d60afd bootstrap 5 2023-08-22 23:03:21 +01:00
Anthony Stirling
8aac0c0327 bs5 more 2023-08-22 21:19:18 +01:00
Anthony Stirling
7c26c56210 test 2023-08-20 21:57:19 +01:00
Anthony Stirling
e88a780efe lang 2023-08-19 17:00:34 +01:00
Anthony Stirling
363fb5dc02 Merge branch 'main' of git@github.com:Frooodle/Stirling-PDF.git into main 2023-08-19 16:59:52 +01:00
Anthony Stirling
39a187b6da darkmode fix for account and pagenumber support filename 2023-08-19 16:59:34 +01:00
Anthony Stirling
b4cc34a522 Merge pull request #321 from appel/main
Dutch translation
2023-08-19 14:41:06 +01:00
appel
af94ef3d49 Dutch translation 2023-08-19 09:25:28 -04:00
Anthony Stirling
505855a53c Fix password updation 2023-08-19 12:50:49 +01:00
Anthony Stirling
87ac245341 Update sanitize-pdf.html 2023-08-19 12:26:43 +01:00
Anthony Stirling
1670a09d04 Update SanitizeController.java 2023-08-19 11:58:18 +01:00
Anthony Stirling
620b954336 Merge pull request #307 from Frooodle/login
Login
2023-08-17 22:48:47 +01:00
Anthony Stirling
cfd51e9b84 Update build.gradle 2023-08-17 22:47:48 +01:00
Anthony Stirling
6c797f8216 fix readme 2023-08-17 22:45:59 +01:00
Anthony Stirling
40e208152a translations 2023-08-17 22:26:35 +01:00
Anthony Stirling
cf7bfa62ef Merge branch 'main' into login 2023-08-17 22:21:31 +01:00
Anthony Stirling
a1086b9a04 update todo 2023-08-17 22:21:14 +01:00
Anthony Stirling
9bc4bbd2c8 grammer 2023-08-17 22:20:50 +01:00
Anthony Stirling
73156012e9 security 2023-08-17 22:19:11 +01:00
Anthony Stirling
91cc3d77d4 readme stuff 2023-08-17 22:17:42 +01:00
Anthony Stirling
3fc55a9e9f translations 2023-08-17 22:03:43 +01:00
Anthony Stirling
b666aa3f26 merge stuff #318 2023-08-17 22:03:36 +01:00
Anthony Stirling
53e7dbe12f translates 2023-08-17 00:10:14 +01:00
Anthony Stirling
9d8ff6856b Merge pull request #316 from deraw/update-fr-translations
Update messages_fr_FR.properties
2023-08-15 08:26:28 +01:00
Dylan Broussard
3fb38376b0 Update messages_fr_FR.properties 2023-08-15 03:38:42 +02:00
Anthony Stirling
15c73d9dd3 Add files via upload 2023-08-15 00:40:22 +01:00
Anthony Stirling
86f71ffb93 change user and pass 2023-08-15 00:39:13 +01:00
Anthony Stirling
eb928d3369 Merge pull request #313 from deraw/deraw-fix-add-page-numbers
Fix title in add-page-numbers.html
2023-08-15 00:02:28 +01:00
Anthony Stirling
d7307665b3 Fix title #313 2023-08-15 00:01:19 +01:00
Dylan Broussard
2836f0ab5a Fix title in add-page-numbers.html
The key `autoCrop.title` was used instead of `addPageNumbers.title`.
2023-08-15 00:29:36 +02:00
Anthony Stirling
91b7f3980c Change account icons 2023-08-14 22:48:30 +01:00
Anthony Stirling
5053432c2d pdf to image 2023-08-14 21:43:39 +01:00
Anthony Stirling
563c612395 remove pbean 2023-08-14 21:42:31 +01:00
Anthony Stirling
95dced6455 Merge branch 'login' of git@github.com:Frooodle/Stirling-PDF.git into login 2023-08-14 21:42:25 +01:00
Anthony Stirling
989f0bbbfb caching 2023-08-14 21:41:42 +01:00
Anthony Stirling
e5eec28bfd Delete mydatabase.trace.db 2023-08-13 22:49:23 +01:00
Anthony Stirling
bfc402f307 Delete mydatabase.mv.db 2023-08-13 22:49:15 +01:00
Anthony Stirling
35a998b934 Login 2023-08-13 22:46:18 +01:00
Anthony Stirling
cadc8e499d IT WORKS almost 2023-08-13 18:19:15 +01:00
Anthony Stirling
7f7ea6da9f APi key stuff 2023-08-13 10:53:00 +01:00
Anthony Stirling
ab9a22d8e7 Add files via upload 2023-08-13 01:15:26 +01:00
Anthony Stirling
cd2728105e Add files via upload 2023-08-13 01:14:30 +01:00
Anthony Stirling
d75e84bdff Create InitialSetup.java 2023-08-13 01:14:14 +01:00
Anthony Stirling
e791fee38b security 2023-08-13 01:12:29 +01:00
Anthony Stirling
ad5f057733 Fix for #306 2023-08-12 19:53:14 +01:00
Anthony Stirling
6f325b5fdb init 2023-08-12 02:29:10 +01:00
Anthony Stirling
b73aeee18d Merge pull request #304 from adrielCarmoUFMS/adrielCarmoUFMS-patch-1-1
Update messages_pt_BR.properties
2023-08-11 16:25:14 +01:00
adrielCarmoUFMS
8a54035a9f Update messages_pt_BR.properties
Update messages_pt_BR.properties
2023-08-11 10:21:13 -04:00
adrielCarmoUFMS
af28e30e4c Update messages_pt_BR.properties
updating new properties for the pt-BR language
2023-08-11 09:23:04 -04:00
Anthony Stirling
492513306c Merge pull request #303 from adrielCarmoUFMS/main
update language pt-br
2023-08-10 21:45:27 +01:00
Anthony Stirling
bc554ff4a7 Merge branch 'main' into main 2023-08-10 21:42:40 +01:00
Anthony Stirling
b7a0d1ece8 Merge pull request #301 from jb2barrels/en_US_Aug-10-2023
Create en_US translation properties
2023-08-10 21:42:16 +01:00
Jacob Braun
ca384218dc Add US to navbar.html and distinguish English selection GB and US
Add US to navbar.html and distinguish English selection GB and US
2023-08-10 13:33:46 -07:00
Jacob Braun
e9550fd6b2 Add US svg
Add US svg
2023-08-10 13:33:20 -07:00
Jacob Braun
232a305d51 Add additional spelling changes for US translations
Add additional spelling changes for US translations
2023-08-10 13:27:56 -07:00
adrielCarmoUFMS
a6ab448eeb Merge pull request #2 from adrielCarmoUFMS/adrielCarmoUFMS-patch-1
Update messages_pt_BR.properties
2023-08-10 16:14:22 -04:00
adrielCarmoUFMS
f9aa157c6c Update messages_pt_BR.properties
Updating the translation of new features to the Portuguese - BR language
2023-08-10 16:03:24 -04:00
adrielCarmoUFMS
b7df24acaa Merge pull request #1 from adrielCarmoUFMS/adrielCarmoUFMS_pt-BR
Update messages_pt_BR.properties
2023-08-10 15:57:59 -04:00
adrielCarmoUFMS
ad4ca1b2d7 Update messages_pt_BR.properties
Updating the translation of new features to the Portuguese - BR language
2023-08-10 15:56:15 -04:00
Jacob Braun
c562d197e7 Create en_US translation properties
Copied from en_GB. Fixed variables noFavourites and home.pdfOrganiser.title to use its US corrected spelling.
2023-08-10 08:56:27 -07:00
Anthony Stirling
83ba1899b7 metric filters 2023-08-09 20:30:19 +01:00
Anthony Stirling
3420adc7c9 security and apple icons 2023-08-09 20:30:07 +01:00
Anthony Stirling
fd39f28e46 Merge pull request #280 from Frooodle/dependabot/gradle/org.springframework.boot-spring-boot-starter-test-3.1.2
Bump org.springframework.boot:spring-boot-starter-test from 3.1.0 to 3.1.2
2023-08-08 20:29:23 +01:00
Anthony Stirling
710125852a Merge branch 'main' into dependabot/gradle/org.springframework.boot-spring-boot-starter-test-3.1.2 2023-08-08 20:27:51 +01:00
Anthony Stirling
e8ec208390 Merge pull request #281 from Frooodle/dependabot/gradle/org.springframework.boot-spring-boot-starter-web-3.1.2
Bump org.springframework.boot:spring-boot-starter-web from 3.1.0 to 3.1.2
2023-08-08 20:27:23 +01:00
dependabot[bot]
4584562607 Bump org.springframework.boot:spring-boot-starter-test
Bumps [org.springframework.boot:spring-boot-starter-test](https://github.com/spring-projects/spring-boot) from 3.1.0 to 3.1.2.
- [Release notes](https://github.com/spring-projects/spring-boot/releases)
- [Commits](https://github.com/spring-projects/spring-boot/compare/v3.1.0...v3.1.2)

---
updated-dependencies:
- dependency-name: org.springframework.boot:spring-boot-starter-test
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-08 19:25:51 +00:00
dependabot[bot]
2c1412a088 Bump org.springframework.boot:spring-boot-starter-web
Bumps [org.springframework.boot:spring-boot-starter-web](https://github.com/spring-projects/spring-boot) from 3.1.0 to 3.1.2.
- [Release notes](https://github.com/spring-projects/spring-boot/releases)
- [Commits](https://github.com/spring-projects/spring-boot/compare/v3.1.0...v3.1.2)

---
updated-dependencies:
- dependency-name: org.springframework.boot:spring-boot-starter-web
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-08 19:25:50 +00:00
Anthony Stirling
0a65382979 Merge pull request #279 from Frooodle/dependabot/gradle/org.springframework.boot-3.1.2
Bump org.springframework.boot from 3.1.1 to 3.1.2
2023-08-08 20:25:32 +01:00
Anthony Stirling
4f404f66e5 Merge branch 'main' into dependabot/gradle/org.springframework.boot-3.1.2 2023-08-08 20:24:34 +01:00
Anthony Stirling
89505ada00 Merge pull request #273 from Frooodle/dependabot/gradle/io.spring.dependency-management-1.1.2
Bump io.spring.dependency-management from 1.1.0 to 1.1.2
2023-08-08 20:24:01 +01:00
dependabot[bot]
374a30ac5a Bump org.springframework.boot from 3.1.1 to 3.1.2
Bumps [org.springframework.boot](https://github.com/spring-projects/spring-boot) from 3.1.1 to 3.1.2.
- [Release notes](https://github.com/spring-projects/spring-boot/releases)
- [Commits](https://github.com/spring-projects/spring-boot/compare/v3.1.1...v3.1.2)

---
updated-dependencies:
- dependency-name: org.springframework.boot
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-08 19:20:25 +00:00
Anthony Stirling
cee4ee4128 Merge branch 'main' into dependabot/gradle/io.spring.dependency-management-1.1.2 2023-08-08 20:19:47 +01:00
Anthony Stirling
58fc5e2ffa Merge pull request #259 from Frooodle/dependabot/gradle/org.apache.pdfbox-pdfbox-2.0.29
Bump org.apache.pdfbox:pdfbox from 2.0.28 to 2.0.29
2023-08-08 20:19:27 +01:00
Anthony Stirling
951ea43f8b Merge branch 'main' into dependabot/gradle/org.apache.pdfbox-pdfbox-2.0.29 2023-08-08 20:09:34 +01:00
Anthony Stirling
ca16ecef24 Create StartupApplicationListener.java 2023-08-08 20:09:05 +01:00
Anthony Stirling
bb025dc2a1 Merge branch 'main' into dependabot/gradle/org.apache.pdfbox-pdfbox-2.0.29 2023-08-08 19:57:32 +01:00
dependabot[bot]
d797169bd0 Bump io.spring.dependency-management from 1.1.0 to 1.1.2
Bumps [io.spring.dependency-management](https://github.com/spring-gradle-plugins/dependency-management-plugin) from 1.1.0 to 1.1.2.
- [Release notes](https://github.com/spring-gradle-plugins/dependency-management-plugin/releases)
- [Commits](https://github.com/spring-gradle-plugins/dependency-management-plugin/compare/v1.1.0...v1.1.2)

---
updated-dependencies:
- dependency-name: io.spring.dependency-management
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-08 18:56:05 +00:00
Anthony Stirling
891f9e2252 api changes 2023-08-08 19:55:18 +01:00
dependabot[bot]
f6262c82e1 Bump org.apache.pdfbox:pdfbox from 2.0.28 to 2.0.29
Bumps org.apache.pdfbox:pdfbox from 2.0.28 to 2.0.29.

---
updated-dependencies:
- dependency-name: org.apache.pdfbox:pdfbox
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-26 21:09:07 +00:00
709 changed files with 91120 additions and 30589 deletions

1
.gitattributes vendored
View File

@@ -1,5 +1,6 @@
# Ignore all JavaScript files in a directory
src/main/resources/static/pdfjs/* linguist-vendored
src/main/resources/static/pdfjs/** linguist-vendored
src/main/resources/static/css/bootstrap-icons.css linguist-vendored
src/main/resources/static/css/bootstrap.min.css linguist-vendored
src/main/resources/static/css/fonts/* linguist-vendored

2
.github/CODEOWNERS vendored Normal file
View File

@@ -0,0 +1,2 @@
# All PRs to V1 must be approved by Frooodle
* @Frooodle

4
.github/pull_request_template.md vendored Normal file
View File

@@ -0,0 +1,4 @@
# License Agreement for Contributions
By submitting this pull request, I acknowledge and agree that my contributions will be included in Stirling-PDF and that they can be relicensed in the future under MPL 2.0 (Mozilla Public License Version 2.0) license.
(This does not change the general open-source nature of Stirling-PDF, simply moving from one license to another license)

View File

@@ -0,0 +1,3 @@
# License Agreement for Contributions
By submitting this pull request, I acknowledge and agree that my contributions will be included in Stirling-PDF and that they can be relicensed in the future under MPL 2.0 (Mozilla Public License Version 2.0) license.
(This does not change the open-source nature of Stirling-PDF, simply moving from one license to another license)

View File

@@ -21,6 +21,8 @@ jobs:
- uses: gradle/gradle-build-action@v2.4.2
env:
DOCKER_ENABLE_SECURITY: false
with:
gradle-version: 7.6
arguments: clean build
@@ -77,6 +79,8 @@ jobs:
cache-to: type=gha,mode=max
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args:
VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
platforms: linux/amd64,linux/arm64/v8
@@ -105,6 +109,8 @@ jobs:
cache-to: type=gha,mode=max
tags: ${{ steps.meta2.outputs.tags }}
labels: ${{ steps.meta2.outputs.labels }}
build-args:
VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
platforms: linux/amd64,linux/arm64/v8
@@ -133,4 +139,10 @@ jobs:
cache-to: type=gha,mode=max
tags: ${{ steps.meta3.outputs.tags }}
labels: ${{ steps.meta3.outputs.labels }}
build-args:
VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
platforms: linux/amd64,linux/arm64/v8
- name: Build and Push Helm Chart
run: |
helm package chart/stirling-pdf
helm push stirling-pdf-chart-1.0.0.tgz oci://registry-1.docker.io/frooodle

View File

@@ -1,10 +1,20 @@
name: Release Artifacts
on:
release:
types: [created]
jobs:
push:
runs-on: ubuntu-latest
strategy:
matrix:
enable_security: [true, false]
include:
- enable_security: true
file_suffix: '-with-login'
- enable_security: false
file_suffix: ''
steps:
- uses: actions/checkout@v3.5.2
@@ -17,15 +27,17 @@ jobs:
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Generate jar
- name: Generate jar (With Security=${{ matrix.enable_security }})
run: ./gradlew clean createExe
env:
DOCKER_ENABLE_SECURITY: ${{ matrix.enable_security }}
- name: Upload binaries to release
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: ./build/launch4j/Stirling-PDF.exe
asset_name: Stirling-PDF.exe
asset_name: Stirling-PDF${{ matrix.file_suffix }}.exe
tag: ${{ github.ref }}
overwrite: true
@@ -33,13 +45,11 @@ jobs:
id: versionNumber
run: echo "::set-output name=versionNumber::$(./gradlew printVersion --quiet | tail -1)"
- name: Upload binaries to release
- name: Upload jar binaries to release
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: ./build/libs/Stirling-PDF-${{ steps.versionNumber.outputs.versionNumber }}.jar
asset_name: Stirling-PDF.jar
asset_name: Stirling-PDF${{ matrix.file_suffix }}.jar
tag: ${{ github.ref }}
overwrite: true

15
.gitignore vendored
View File

@@ -15,11 +15,11 @@ local.properties
.classpath
.project
version.properties
pipeline/
pipeline/watchedFolders/
pipeline/finishedFolders/
#### Stirling-PDF Files ###
customFiles/
config/
configs/
watchedFolders/
@@ -116,7 +116,12 @@ watchedFolders/
*.zip
*.tar.gz
*.rar
*.db
/build
/.vscode
/.vscode
/.idea
# Ignore Mac DS_Store files
.DS_Store
**/.DS_Store

View File

@@ -1,28 +1,47 @@
# Build jbig2enc in a separate stage
FROM frooodle/stirling-pdf-base:beta4
# Use the base image
FROM frooodle/stirling-pdf-base:version8
# Create scripts folder and copy local scripts
RUN mkdir /scripts
ARG VERSION_TAG
# Set Environment Variables
ENV DOCKER_ENABLE_SECURITY=false \
HOME=/home/stirlingpdfuser \
VERSION_TAG=$VERSION_TAG \
JAVA_TOOL_OPTIONS="$JAVA_TOOL_OPTIONS -XX:MaxRAMPercentage=75"
# PUID=1000 \
# PGID=1000 \
# UMASK=022 \
# Create user and group
##RUN groupadd -g $PGID stirlingpdfgroup && \
## useradd -u $PUID -g stirlingpdfgroup -s /bin/sh stirlingpdfuser && \
## mkdir -p $HOME && chown stirlingpdfuser:stirlingpdfgroup $HOME
# Set up necessary directories and permissions
RUN mkdir -p /scripts /usr/share/fonts/opentype/noto /usr/share/tesseract-ocr /configs /logs /customFiles /pipeline /pipeline/defaultWebUIConfigs /pipeline/watchedFolders /pipeline/finishedFolders
##&& \
## chown -R stirlingpdfuser:stirlingpdfgroup /scripts /usr/share/fonts/opentype/noto /usr/share/tesseract-ocr /configs /customFiles && \
## chown -R stirlingpdfuser:stirlingpdfgroup /usr/share/tesseract-ocr-original
# Copy necessary files
COPY ./scripts/* /scripts/
#Install fonts
RUN mkdir /usr/share/fonts/opentype/noto/
COPY ./pipeline/ /pipeline/
COPY src/main/resources/static/fonts/*.ttf /usr/share/fonts/opentype/noto/
COPY src/main/resources/static/fonts/*.otf /usr/share/fonts/opentype/noto/
RUN fc-cache -f -v
# Copy the application JAR file
COPY build/libs/*.jar app.jar
# Expose the application port
# Set font cache and permissions
RUN fc-cache -f -v && chmod +x /scripts/*
##&& \
## chown stirlingpdfuser:stirlingpdfgroup /app.jar && \
## chmod +x /scripts/init.sh
# Expose necessary ports
EXPOSE 8080
# Set environment variables
ENV APP_HOME_NAME="Stirling PDF"
#ENV APP_HOME_DESCRIPTION="Personal PDF Website!"
#ENV APP_NAVBAR_NAME="Stirling PDF"
# Run the application
RUN chmod +x /scripts/init.sh
# Set user and run command
##USER stirlingpdfuser
ENTRYPOINT ["/scripts/init.sh"]
CMD ["java", "-jar", "/app.jar"]
CMD ["java", "-Dfile.encoding=UTF-8", "-jar", "/app.jar"]

View File

@@ -1,23 +1,65 @@
# Build jbig2enc in a separate stage
FROM bellsoft/liberica-openjdk-debian:17
ARG VERSION_TAG
RUN apt-get update && \
apt-get install -y --no-install-recommends \
libreoffice-core-nogui \
libreoffice-core \
libreoffice-common \
libreoffice-writer-nogui \
libreoffice-calc-nogui \
libreoffice-impress-nogui \
libreoffice-writer \
libreoffice-calc \
libreoffice-impress \
unoconv && \
rm -rf /var/lib/apt/lists/*
# Copy the application JAR file
# Set Environment Variables
ENV DOCKER_ENABLE_SECURITY=false \
HOME=/home/stirlingpdfuser \
VERSION_TAG=$VERSION_TAG \
JAVA_TOOL_OPTIONS="$JAVA_TOOL_OPTIONS -XX:MaxRAMPercentage=75"
# PUID=1000 \
# PGID=1000 \
# UMASK=022 \
# Create user and group
#RUN groupadd -g $PGID stirlingpdfgroup && \
# useradd -u $PUID -g stirlingpdfgroup -s /bin/sh stirlingpdfuser && \
# mkdir -p $HOME && chown stirlingpdfuser:stirlingpdfgroup $HOME
# Set up necessary directories and permissions
RUN mkdir -p /scripts /usr/share/fonts/opentype/noto /configs /customFiles /logs /pipeline /pipeline/defaultWebUIConfigs /pipeline/watchedFolders /pipeline/finishedFolders
# chown -R stirlingpdfuser:stirlingpdfgroup /usr/share/fonts/opentype/noto /configs /customFiles
# Copy necessary files
COPY ./scripts/download-security-jar.sh /scripts/download-security-jar.sh
COPY ./scripts/init-without-ocr.sh /scripts/init-without-ocr.sh
COPY ./pipeline/ /pipeline/
COPY src/main/resources/static/fonts/*.ttf /usr/share/fonts/opentype/noto/
COPY src/main/resources/static/fonts/*.otf /usr/share/fonts/opentype/noto/
COPY build/libs/*.jar app.jar
# Set font cache and permissions
RUN fc-cache -f -v && \
chmod +x /scripts/init-without-ocr.sh && \
chmod +x /scripts/download-security-jar.sh
# chown stirlingpdfuser:stirlingpdfgroup /app.jar
# Expose the application port
EXPOSE 8080
# Set environment variables
ENV GROUPS_TO_REMOVE=Python,OpenCV,OCRmyPDF
ENV ENDPOINTS_GROUPS_TO_REMOVE=Python,OpenCV,OCRmyPDF
ENV DOCKER_ENABLE_SECURITY=false
# Run the application
CMD ["java", "-jar", "/app.jar"]
#USER stirlingpdfuser
ENTRYPOINT ["/scripts/init-without-ocr.sh"]
CMD ["java", "-Dfile.encoding=UTF-8", "-jar", "/app.jar"]

View File

@@ -1,14 +1,46 @@
# Build jbig2enc in a separate stage
FROM bellsoft/liberica-openjdk-alpine:17
# Copy the application JAR file
ARG VERSION_TAG
# Set Environment Variables
ENV DOCKER_ENABLE_SECURITY=false \
HOME=/home/stirlingpdfuser \
VERSION_TAG=$VERSION_TAG \
JAVA_TOOL_OPTIONS="$JAVA_TOOL_OPTIONS -XX:MaxRAMPercentage=75"
# PUID=1000 \
# PGID=1000 \
# UMASK=022 \
# Create user and group using Alpine's addgroup and adduser
#RUN addgroup -g $PGID stirlingpdfgroup && \
# adduser -u $PUID -G stirlingpdfgroup -s /bin/sh -D stirlingpdfuser && \
# mkdir -p $HOME && chown stirlingpdfuser:stirlingpdfgroup $HOME
# Set up necessary directories and permissions
#RUN mkdir -p /scripts /configs /customFiles && \
# chown -R stirlingpdfuser:stirlingpdfgroup /scripts /configs /customFiles /logs /pipeline /pipeline/defaultWebUIConfigs /pipeline/watchedFolders /pipeline/finishedFolders
RUN mkdir -p /scripts /usr/share/fonts/opentype/noto /configs /customFiles
COPY ./scripts/download-security-jar.sh /scripts/download-security-jar.sh
COPY ./scripts/init-without-ocr.sh /scripts/init-without-ocr.sh
COPY ./pipeline/ /pipeline/
COPY build/libs/*.jar app.jar
# Set font cache and permissions
#RUN chown stirlingpdfuser:stirlingpdfgroup /app.jar
RUN chmod +x /scripts/init-without-ocr.sh && \
chmod +x /scripts/download-security-jar.sh && \
apk add --no-cache curl
# Expose the application port
EXPOSE 8080
# Set environment variables
ENV GROUPS_TO_REMOVE=CLI
ENV ENDPOINTS_GROUPS_TO_REMOVE=CLI
ENTRYPOINT ["/scripts/init-without-ocr.sh"]
# Run the application
CMD ["java", "-jar", "/app.jar"]
CMD ["java", "-Dfile.encoding=UTF-8", "-jar", "/app.jar"]

View File

@@ -1,37 +1,50 @@
# Main stage
FROM bellsoft/liberica-openjdk-debian:17 AS base
FROM ubuntu:latest AS base
# JDK for app
RUN apt-get update && \
apt-get install -y --no-install-recommends \
libreoffice-core-nogui \
openjdk-17-jre
# Doc conversion
RUN apt-get update && \
apt-get install -y --no-install-recommends \
libreoffice-core \
libreoffice-common \
libreoffice-writer-nogui \
libreoffice-calc-nogui \
libreoffice-impress-nogui \
python3-uno \
libreoffice-writer \
libreoffice-calc \
libreoffice-impress \
python3-uno \
curl \
unoconv
# OCR MY PDF (unpaper for descew and other advanced featues)
RUN apt-get update && apt-get install -y --no-install-recommends software-properties-common gnupg2 && \
add-apt-repository ppa:alex-p/tesseract-ocr5 && apt install -y --no-install-recommends tesseract-ocr && \
apt-get update && \
apt-get install -y --no-install-recommends \
ghostscript \
python3-pip \
unoconv \
pngquant \
unpaper \
ocrmypdf && \
rm -rf /var/lib/apt/lists/* && \
ocrmypdf \
unpaper && \
pip install --upgrade pip && \
pip install --no-cache-dir --upgrade ocrmypdf && \
pip install --no-cache-dir --upgrade pillow==10.0.1 reportlab==3.6.13 wheel==0.38.1 setuptools==65.5.1 pyjwt==2.4.0 cryptography==39.0.1
#CV and HTML
RUN pip install --no-cache-dir opencv-python-headless WeasyPrint
# cleanup and etc
RUN rm -rf /var/lib/apt/lists/* && \
mkdir /usr/share/tesseract-ocr-original && \
cp -r /usr/share/tesseract-ocr/* /usr/share/tesseract-ocr-original && \
rm -rf /usr/share/tesseract-ocr
# Python packages stage
FROM base AS python-packages
RUN apt-get update && \
apt-get install -y --no-install-recommends \
build-essential \
libffi-dev \
libssl-dev \
zlib1g-dev \
libjpeg-dev && \
pip install --upgrade pip && \
pip install --no-cache-dir \
opencv-python-headless WeasyPrint && \
rm -rf /var/lib/apt/lists/*
# Final stage: Copy necessary files from the previous stage
FROM base
COPY --from=python-packages /usr/local /usr/local

View File

@@ -8,7 +8,7 @@ Fork Stirling-PDF and make a new branch out of Main
Then add reference to the language in the navbar by adding a new language entry to the dropdown
https://github.com/Frooodle/Stirling-PDF/blob/main/src/main/resources/templates/fragments/navbar.html#L306
https://github.com/Frooodle/Stirling-PDF/blob/main/src/main/resources/templates/fragments/languages.html
and add a flag svg file to
https://github.com/Frooodle/Stirling-PDF/tree/main/src/main/resources/static/images/flags
Any SVG flags are fine, i got most of mine from [here](https://flagicons.lipis.dev/)
@@ -25,7 +25,7 @@ The data-language-code is the code used to reference the file in the next step.
Start by copying the existing english property file
[https://github.com/Frooodle/Stirling-PDF/tree/langSetup/src/main/resources/messages_en_GB.properties](https://github.com/Frooodle/Stirling-PDF/blob/main/src/main/resources/messages_en_US.properties)
[https://github.com/Frooodle/Stirling-PDF/blob/main/src/main/resources/messages_en_GB.properties](https://github.com/Frooodle/Stirling-PDF/blob/main/src/main/resources/messages_en_GB.properties)
Copy and rename it to messages_{your data-language-code here}.properties, in the polish example you would set the name to messages_pl_PL.properties

View File

@@ -2,6 +2,9 @@
This document provides instructions on how to add additional language packs for the OCR tab in Stirling-PDF, both inside and outside of Docker.
## My OCR used to work and now doesnt!
Please update your tesseract docker volume path version from 4.00 to 5
## How does the OCR Work
Stirling-PDF uses [OCRmyPDF](https://github.com/ocrmypdf/OCRmyPDF) which in turn uses tesseract for its text recognition.
All credit goes to them for this awesome work!
@@ -18,9 +21,9 @@ Depending on your requirements, you can choose the appropriate language pack for
### Installing Language Packs
1. Download the desired language pack(s) by selecting the `.traineddata` file(s) for the language(s) you need.
2. Place the `.traineddata` files in the Tesseract tessdata directory: `/usr/share/tesseract-ocr/4.00/tessdata` (Debian) or `/usr/share/tesseract/tessdata` (Fedora)
2. Place the `.traineddata` files in the Tesseract tessdata directory: `/usr/share/tesseract-ocr/5/tessdata` (Debian) or `/usr/share/tesseract/tessdata` (Fedora)
# DO NOT REMOVE EXISTING ENG.TRAINEDDATA, ITS REQUIRED.
# DO NOT REMOVE EXISTING ENG.TRAINEDDATA, IT'S REQUIRED.
#### Docker
@@ -34,14 +37,14 @@ services:
your_service_name:
image: your_docker_image_name
volumes:
- /location/of/trainingData:/usr/share/tesseract-ocr/4.00/tessdata
- /location/of/trainingData:/usr/share/tesseract-ocr/5/tessdata
```
#### Docker run
Add the following to your existing docker run command
```bash
-v /location/of/trainingData:/usr/share/tesseract-ocr/4.00/tessdata
-v /location/of/trainingData:/usr/share/tesseract-ocr/5/tessdata
```
#### Non-Docker

20
Jenkinsfile vendored
View File

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

View File

@@ -1,5 +1,5 @@
To run the application without Docker, you will need to manually install all dependencies and build the necessary components.
To run the application without Docker/Podman, you will need to manually install all dependencies and build the necessary components.
Note that some dependencies might not be available in the standard repositories of all Linux distributions, and may require additional steps to install.
@@ -8,6 +8,8 @@ The following guide assumes you have a basic understanding of using a command li
It should work on most Linux distributions and MacOS. For Windows, you might need to use Windows Subsystem for Linux (WSL) for certain steps.
The amount of dependencies is to actually reduce overall size, ie installing LibreOffice sub components rather than full LibreOffice package.
You could theoretically use a Distrobox/Toolbox, if your Distribution has old or not all Packages. But you might just as well use the Docker Container then.
### Step 1: Prerequisites
Install the following software, if not already installed:
@@ -18,7 +20,7 @@ Install the following software, if not already installed:
- Git
- Python 3 (with pip)
- Python 3.8 (with pip)
- Make
@@ -93,14 +95,14 @@ For Debian-based systems, you can use the following command:
```bash
sudo apt-get install -y libreoffice-writer libreoffice-calc libreoffice-impress unpaper ocrmypdf
pip3 install uno opencv-python-headless unoconv pngquant
pip3 install uno opencv-python-headless unoconv pngquant WeasyPrint
```
For Fedora:
```bash
sudo dnf install -y libreoffice-writer libreoffice-calc libreoffice-impress unpaper ocrmypdf
pip3 install uno opencv-python-headless unoconv pngquant
pip3 install uno opencv-python-headless unoconv pngquant WeasyPrint
```
### Step 4: Clone and Build Stirling-PDF
@@ -137,7 +139,7 @@ Easiest is to use the langpacks provided by your repositories. Skip the other st
Manual:
1. Download the desired language pack(s) by selecting the `.traineddata` file(s) for the language(s) you need.
2. Place the `.traineddata` files in the Tesseract tessdata directory: `/usr/share/tesseract-ocr/4.00/tessdata`
2. Place the `.traineddata` files in the Tesseract tessdata directory: `/usr/share/tesseract-ocr/5/tessdata`
3.
Please view [OCRmyPDF install guide](https://ocrmypdf.readthedocs.io/en/latest/installation.html) for more info.
**IMPORTANT:** DO NOT REMOVE EXISTING `eng.traineddata`, IT'S REQUIRED.
@@ -174,7 +176,7 @@ rpm -qa | grep tesseract-langpack | sed 's/tesseract-langpack-//g'
```bash
./gradlew bootRun
or
java -jar build/libs/app.jar
java -jar /opt/Stirling-PDF/Stirling-PDF-*.jar
```
### Step 8: Adding a Desktop icon
@@ -200,6 +202,64 @@ EOF
Note: Currently the app will run in the background until manually closed.
### Optional: Run Stirling-PDF as a service
First create a .env file, where you can store environment variables:
```
touch /opt/Stirling-PDF/.env
```
In this file you can add all variables, one variable per line, as stated in the main readme (for example SYSTEM_DEFAULTLOCALE="de-DE").
Create a new file where we store our service settings and open it with nano editor:
```
nano /etc/systemd/system/stirlingpdf.service
```
Paste this content, make sure to update the filename of the jar-file. Press Ctrl+S and Ctrl+X to save and exit the nano editor:
```
[Unit]
Description=Stirling-PDF service
After=syslog.target network.target
[Service]
SuccessExitStatus=143
User=root
Group=root
Type=simple
EnvironmentFile=/opt/Stirling-PDF/.env
WorkingDirectory=/opt/Stirling-PDF
ExecStart=/usr/bin/java -jar Stirling-PDF-0.17.2.jar
ExecStop=/bin/kill -15 $MAINPID
[Install]
WantedBy=multi-user.target
```
Notify systemd that it has to rebuild its internal service database (you have to run this command every time you make a change in the service file):
```
sudo systemctl daemon-reload
```
Enable the service to tell the service to start it automatically:
```
sudo systemctl enable stirlingpdf.service
```
See the status of the service:
```
sudo systemctl status stirlingpdf.service
```
Manually start/stop/restart the service:
```
sudo systemctl start stirlingpdf.service
sudo systemctl stop stirlingpdf.service
sudo systemctl restart stirlingpdf.service
```
---
Remember to set the necessary environment variables before running the project if you want to customize the application the list can be seen in the main readme.

240
README.md
View File

@@ -14,59 +14,78 @@ This is a powerful locally hosted web based PDF manipulation tool using docker t
Stirling PDF makes no outbound calls for any record keeping or tracking.
All files and PDFs are either purely client side, in server memory only during the execution of the task or within a temporay file only for execution of the task.
Any file which has been downloaded by the user will have already been deleted from the server by that time.
Feel free to request any features or bug fixes either in github issues or our [Discord](https://discord.gg/Cn8pWhQRxZ)
All files and PDFs exist either exclusively on the client side, reside in server memory only during task execution, or temporarily reside in a file solely for the execution of the task. Any file downloaded by the user will have been deleted from the server by that point.
Please feel free to submit feature requests or report bugs either through GitHub issues or on our [Discord](https://discord.gg/Cn8pWhQRxZ)
![stirling-home](images/stirling-home.png)
## Features
- Full interactive GUI for merging/splitting/rotating/moving PDFs and their pages.
- Split PDFs into multiple files at specified page numbers or extract all pages as individual files.
- Merge multiple PDFs together into a single resultant file
- Convert PDFs to and from images
- Reorganize PDF pages into different orders.
- Add/Generate signatures
- Format PDFs into a multi-paged page
- Scale page contents size by set %
- Adjust Contrast
- Crop PDF
- Auto Split PDF (With physically scanned page dividers)
- Flatten PDFs
- Repair PDFs
- Detect and remove blank pages
- Compare 2 PDFs and show differences in text
- Add images to PDFs
- Rotating PDFs in 90 degree increments.
- Compressing PDFs to decrease their filesize. (Using OCRMyPDF)
- Add and remove passwords
- Set PDF Permissions
- Add watermark(s)
- Convert Any common file to PDF (using LibreOffice)
- Convert PDF to Word/Powerpoint/Others (using LibreOffice)
- Convert HTML to PDF
- URL to PDF
- Extract images from PDF
- Extract images from Scans
- Add page numbers
- Auto rename file by detecting PDF header text
- OCR on PDF (Using OCRMyPDF)
- PDF/A conversion (Using OCRMyPDF)
- Edit metadata
- Dark mode support.
- Custom download options (see [here](https://github.com/Frooodle/Stirling-PDF/blob/main/images/settings.png) for example)
- Parallel file processing and downloads
- API for integration with external scripts
- Optional Login and Authentication support (see [here](https://github.com/Frooodle/Stirling-PDF/tree/main#login-authentication) for documentation)
For a overview of the tasks and the technology each uses please view [groups.md](https://github.com/Frooodle/Stirling-PDF/blob/main/Groups.md)
## **PDF Features**
### **Page Operations**
- View and modify PDFs - View multi page PDFs with custom viewing sorting and searching. Plus on page edit features like annotate, draw and adding text and images. (Using PDF.js with Joxit and Liberation.Liberation fonts)
- Full interactive GUI for merging/splitting/rotating/moving PDFs and their pages.
- Merge multiple PDFs together into a single resultant file.
- Split PDFs into multiple files at specified page numbers or extract all pages as individual files.
- Reorganize PDF pages into different orders.
- Rotate PDFs in 90-degree increments.
- Remove pages.
- Multi-page layout (Format PDFs into a multi-paged page).
- Scale page contents size by set %.
- Adjust Contrast.
- Crop PDF.
- Auto Split PDF (With physically scanned page dividers).
- Extract page(s).
- Convert PDF to a single page.
### **Conversion Operations**
- Convert PDFs to and from images.
- Convert any common file to PDF (using LibreOffice).
- Convert PDF to Word/Powerpoint/Others (using LibreOffice).
- Convert HTML to PDF.
- URL to PDF.
- Markdown to PDF.
### **Security & Permissions**
- Add and remove passwords.
- Change/set PDF Permissions.
- Add watermark(s).
- Certify/sign PDFs.
- Sanitize PDFs.
- Auto-redact text.
### **Other Operations**
- Add/Generate/Write signatures.
- Repair PDFs.
- Detect and remove blank pages.
- Compare 2 PDFs and show differences in text.
- Add images to PDFs.
- Compress PDFs to decrease their filesize (Using OCRMyPDF).
- Extract images from PDF.
- Extract images from Scans.
- Add page numbers.
- Auto rename file by detecting PDF header text.
- OCR on PDF (Using OCRMyPDF).
- PDF/A conversion (Using OCRMyPDF).
- Edit metadata.
- Flatten PDFs.
- Get all information on a PDF to view or export as JSON.
For a overview of the tasks and the technology each uses please view [Endpoint-groups.md](https://github.com/Frooodle/Stirling-PDF/blob/main/Endpoint-groups.md)
Hosted instance/demo of the app can be seen [here](https://pdf.adminforge.de/) hosted by the team at adminforge.de
## Technologies used
- Spring Boot + Thymeleaf
- PDFBox
- IText7
- [LibreOffice](https://www.libreoffice.org/discover/libreoffice/) for advanced conversions
- [OcrMyPdf](https://github.com/ocrmypdf/OCRmyPDF)
- HTML, CSS, JavaScript
@@ -79,12 +98,12 @@ Hosted instance/demo of the app can be seen [here](https://pdf.adminforge.de/) h
### Locally
Please view https://github.com/Frooodle/Stirling-PDF/blob/main/LocalRunGuide.md
### Docker
### Docker / Podman
https://hub.docker.com/r/frooodle/s-pdf
Stirling PDF has 3 different versions, a Full version, Lite and ultra-Lite. Depending on the types of features you use you may want a smaller image to save on space.
Stirling PDF has 3 different versions, a Full version, Lite, and ultra-Lite. Depending on the types of features you use you may want a smaller image to save on space.
To see what the different versions offer please look at our [version mapping](https://github.com/Frooodle/Stirling-PDF/blob/main/Version-groups.md)
For people that dont mind about space optimisation just use latest tag.
For people that don't mind about space optimization just use the latest tag.
![Docker Image Size (tag)](https://img.shields.io/docker/image-size/frooodle/s-pdf/latest?label=Stirling-PDF%20Full)
![Docker Image Size (tag)](https://img.shields.io/docker/image-size/frooodle/s-pdf/latest-lite?label=Stirling-PDF%20Lite)
![Docker Image Size (tag)](https://img.shields.io/docker/image-size/frooodle/s-pdf/latest-ultra-lite?label=Stirling-PDF%20Ultra-Lite)
@@ -93,20 +112,17 @@ Docker Run
```
docker run -d \
-p 8080:8080 \
-v /location/of/trainingData:/usr/share/tesseract-ocr/4.00/tessdata \
-v /location/of/trainingData:/usr/share/tesseract-ocr/5/tessdata \
-v /location/of/extraConfigs:/configs \
-v /location/of/logs:/logs \
-e DOCKER_ENABLE_SECURITY=false \
--name stirling-pdf \
frooodle/s-pdf:latest
Can also add these for customisation but are not required
-v /location/of/extraConfigs:/configs \
-v /location/of/customFiles:/customFiles \
-e APP_HOME_NAME="Stirling PDF" \
-e APP_HOME_DESCRIPTION="Your locally hosted one-stop-shop for all your PDF needs." \
-e APP_NAVBAR_NAME="Stirling PDF" \
-e ALLOW_GOOGLE_VISIBILITY="true" \
-e APP_ROOT_PATH="/" \
-e APP_LOCALE="en_GB" \
```
Docker Compose
```
@@ -117,26 +133,23 @@ services:
ports:
- '8080:8080'
volumes:
- /location/of/trainingData:/usr/share/tesseract-ocr/4.00/tessdata #Required for extra OCR languages
# - /location/of/extraConfigs:/configs
- /location/of/trainingData:/usr/share/tesseract-ocr/5/tessdata #Required for extra OCR languages
- /location/of/extraConfigs:/configs
# - /location/of/customFiles:/customFiles/
# environment:
# APP_LOCALE: en_GB
# APP_HOME_NAME: Stirling PDF
# APP_HOME_DESCRIPTION: Your locally hosted one-stop-shop for all your PDF needs.
# APP_NAVBAR_NAME: Stirling PDF
# APP_ROOT_PATH: /
# ALLOW_GOOGLE_VISIBILITY: true
# - /location/of/logs:/logs/
environment:
- DOCKER_ENABLE_SECURITY=false
```
Note: Podman is CLI-compatible with Docker, so simply replace "docker" with "podman".
## Enable OCR/Compression feature
Please view https://github.com/Frooodle/Stirling-PDF/blob/main/HowToUseOCR.md
## Want to add your own language?
Stirling PDF currently supports 16!
Stirling PDF currently supports 21!
- English (English) (en_GB)
- English (US) (en_US)
- Arabic (العربية) (ar_AR)
- German (Deutsch) (de_DE)
- French (Français) (fr_FR)
@@ -152,53 +165,110 @@ Stirling PDF currently supports 16!
- Russian (Русский) (ru_RU)
- Basque (Euskara) (eu_ES)
- Japanese (日本語) (ja_JP)
- Dutch (Nederlands) (nl_NL)
- Greek (el_GR)
- Turkish (Türkçe) (tr_TR)
- Indonesia (Bahasa Indonesia) (id_ID)
- Hindi (हिंदी) (hi_IN)
If you want to add your own language to Stirling-PDF please refer
https://github.com/Frooodle/Stirling-PDF/blob/main/HowToAddNewLanguage.md
And please create a PR to merge it back in so others can use it!
Also please note as i add new features i will google translate existing languages so that they dont lose support. This could mean that new features need grammer corrections as added.
## How to View
1. Open a web browser and navigate to `http://localhost:8080/`
2. Use the application by following the instructions on the website.
## Customize App
Stirling PDF allows easy customization of the visible application name.
Simply use environment variables APP_HOME_NAME, APP_HOME_DESCRIPTION and APP_NAVBAR_NAME with Docker or Java.
If running Java directly, you can also pass these as properties using -D arguments.
## Customisation
Stirling PDF allows easy customization of the app.
Includes things like
- Custom application name
- Custom slogans, icons, images, and even custom HTML (via file overrides)
Using the same method you can also change
- The default language by providing APP_LOCALE with values like de-DE fr-FR or ar-AR (Note the - character not _ ) to select your default language (Will always default to English on invalid locale) Current accepted locales can be seen above in the Want to add your own language section
- Enable/Disable search engine visiblility with ALLOW_GOOGLE_VISIBILITY with true / false values. Default disable visiblility.
- Change root URI for Stirling-PDF ie change server.com/ to server.com/pdf-app by running APP_ROOT_PATH as pdf-app
- Disable and remove endpoints and functionality from Stirling-PDF. Currently the endpoints ENDPOINTS_TO_REMOVE and GROUPS_TO_REMOVE can include comma seperated lists of endpoints and groups to disable as example ENDPOINTS_TO_REMOVE=img-to-pdf,remove-pages would disable both image to pdf and remove pages, GROUPS_TO_REMOVE=LibreOffice Would disable all things that use LibreOffice. You can see a list of all endpoints and groups [here](https://github.com/Frooodle/Stirling-PDF/blob/main/groups.md)
- Change the max file size allowed through the server with the environment variable MAX_FILE_SIZE. default 2000MB
- Customise static files such as app logo by placing files in the /customFiles/static/ directory. Example to customise app logo is placing a /customFiles/static/favicon.svg to override current SVG. This can be used to change any images/icons/css/fonts/js etc in Stirling-PDF
There are two options for this, either using the generated settings file ``settings.yml``
This file is located in the ``/configs`` directory and follows standard YAML formatting
Environment variables are also supported and would override the settings file
For example in the settings.yml you have
```
system:
defaultLocale: 'en-US'
```
To have this via an environment variable you would have ``SYSTEM_DEFAULTLOCALE``
The Current list of settings is
```
security:
enableLogin: false # set to 'true' to enable login
csrfDisabled: true
system:
defaultLocale: 'en-US' # Set the default language (e.g. 'de-DE', 'fr-FR', etc)
googlevisibility: false # 'true' to allow Google visibility (via robots.txt), 'false' to disallow
customStaticFilePath: '/customFiles/static/' # Directory path for custom static files
#ui:
# appName: exampleAppName # Application's visible name
# homeDescription: I am a description # Short description or tagline shown on homepage.
# appNameNavbar: navbarName # Name displayed on the navigation bar
endpoints:
toRemove: [] # List endpoints to disable (e.g. ['img-to-pdf', 'remove-pages'])
groupsToRemove: [] # List groups to disable (e.g. ['LibreOffice'])
metrics:
enabled: true # 'true' to enable Info APIs endpoints (view http://localhost:8080/swagger-ui/index.html#/API to learn more), 'false' to disable
```
### Extra notes
- Endpoints. Currently, the endpoints ENDPOINTS_TO_REMOVE and GROUPS_TO_REMOVE can include comma separate lists of endpoints and groups to disable as example ENDPOINTS_TO_REMOVE=img-to-pdf,remove-pages would disable both image-to-pdf and remove pages, GROUPS_TO_REMOVE=LibreOffice Would disable all things that use LibreOffice. You can see a list of all endpoints and groups [here](https://github.com/Frooodle/Stirling-PDF/blob/main/Endpoint-groups.md)
- customStaticFilePath. Customise static files such as the app logo by placing files in the /customFiles/static/ directory. An example of customising app logo is placing a /customFiles/static/favicon.svg to override current SVG. This can be used to change any images/icons/css/fonts/js etc in Stirling-PDF
### Environment only parameters
- ``SYSTEM_ROOTURIPATH`` ie set to ``/pdf-app`` to Set the application's root URI to ``localhost:8080/pdf-app``
- ``SYSTEM_CONNECTIONTIMEOUTMINUTES`` to set custom connection timeout values
- ``DOCKER_ENABLE_SECURITY`` to tell docker to download security jar (required as true for auth login)
## API
For those wanting to use Stirling-PDFs backend API to link with their own custom scripting to edit PDFs you can view all existing API documentation
[here](https://app.swaggerhub.com/apis-docs/Frooodle/Stirling-PDF/) or navigate to /swagger-ui/index.html of your stirling-pdf instance for your versions documentation (Or by following the API button in your settings of Stirling-PDF)
## Login authentication
![stirling-login](images/login-light.png)
### Prerequisites:
- User must have the folder ./configs volumed within docker so that it is retained during updates.
- Docker uses must download the security jar version by setting ``DOCKER_ENABLE_SECURITY`` to ``true`` in environment variables.
- Then either enable login via the settings.yml file or via setting ``SECURITY_ENABLE_LOGIN`` to ``true``
- Now the initial user will be generated with username ``admin`` and password ``stirling``. On login you will be forced to change the password to a new one. You can also use the environment variables ``SECURITY_INITIALLOGIN_USERNAME`` and ``SECURITY_INITIALLOGIN_PASSWORD`` to set your own straight away (Recommended to remove them after user creation).
Once the above has been done, on restart, a new stirling-pdf-DB.mv.db will show if everything worked.
When you login to Stirling PDF you will be redirected to /login page to login with those default credentials. After login everything should function as normal
To access your account settings go to Account settings in the settings cog menu (top right in navbar) This Account settings menu is also where you find your API key.
To add new users go to the bottom of Account settings and hit 'Admin Settings', here you can add new users. The different roles mentioned within this are for rate limiting. This is a Work in progress which will be expanding on more in future
For API usage you must provide a header with 'X-API-Key' and the associated API key for that user.
## FAQ
### Q1: Can you add authentication in Stirling PDF?
There is no Auth within Stirling PDF and there is none planned. This feature will not be added. Instead we recommended you use trusted and secure authentication software like Authentik or Authelia.
### Q2: What are your planned features?
- Crop
### Q1: What are your planned features?
- Progress bar/Tracking
- Full custom logic pipelines to combine multiple operations together.
- Folder support with auto scanning to perform operations on
- Redact sections of pages
- Add page numbers
- Auto rename (Renames file based on file title text)
- URL to PDF
- Change contrast
- Redact text (Via UI not just automated way)
- Add Forms
- Multi page layout (Stich PDF pages together) support x rows y columns and custom page sizing
- Fill forms mannual and automatic
### Q3: Why is my application downloading .htm files?
This is a issue caused commonly by your NGINX congifuration. The default file upload size for NGINX is 1MB, you need to add the following in your Nginx sites-available file. client_max_body_size SIZE; Where "SIZE" is 50M for example for 50MB files.
### Q2: Why is my application downloading .htm files?
This is an issue caused commonly by your NGINX configuration. The default file upload size for NGINX is 1MB, you need to add the following in your Nginx sites-available file. ``client_max_body_size SIZE;`` Where "SIZE" is 50M for example for 50MB files.
### Q3: Why is my download timing out
NGINX has timeout values by default so if you are running Stirling-PDF behind NGINX you may need to set a timeout value such as adding the config ``proxy_read_timeout 3600;``

View File

@@ -19,6 +19,7 @@ add-image | ✔️ | ✔️ | ✔️
add-watermark | ✔️ | ✔️ | ✔️
adjust-contrast | ✔️ | ✔️ | ✔️
auto-split-pdf | ✔️ | ✔️ | ✔️
auto-redact | ✔️ | ✔️ | ✔️
auto-rename | ✔️ | ✔️ | ✔️
cert-sign | ✔️ | ✔️ | ✔️
crop | ✔️ | ✔️ | ✔️
@@ -33,7 +34,9 @@ img-to-pdf | ✔️ | ✔️ | ✔️
markdown-to-pdf | ✔️ | ✔️ | ✔️
merge-pdfs | ✔️ | ✔️ | ✔️
multi-page-layout | ✔️ | ✔️ | ✔️
overlay-pdf | ✔️ | ✔️ | ✔️
pdf-organizer | ✔️ | ✔️ | ✔️
pdf-to-csv | ✔️ | ✔️ | ✔️
pdf-to-img | ✔️ | ✔️ | ✔️
pdf-to-single-page | ✔️ | ✔️ | ✔️
remove-pages | ✔️ | ✔️ | ✔️
@@ -43,6 +46,8 @@ sanitize-pdf | ✔️ | ✔️ | ✔️
scale-pages | ✔️ | ✔️ | ✔️
sign | ✔️ | ✔️ | ✔️
show-javascript | ✔️ | ✔️ | ✔️
split-by-size-or-count | ✔️ | ✔️ | ✔️
split-pdf-by-sections | ✔️ | ✔️ | ✔️
split-pdfs | ✔️ | ✔️ | ✔️
file-to-pdf | | ✔️ | ✔️
pdf-to-html | | ✔️ | ✔️

View File

@@ -1,22 +1,39 @@
plugins {
id 'java'
id 'org.springframework.boot' version '3.1.1'
id 'io.spring.dependency-management' version '1.1.0'
id 'org.springdoc.openapi-gradle-plugin' version '1.6.0'
id "io.swagger.swaggerhub" version "1.2.0"
id 'edu.sc.seis.launch4j' version '3.0.3'
id 'org.springframework.boot' version '3.1.2'
id 'io.spring.dependency-management' version '1.1.3'
id 'org.springdoc.openapi-gradle-plugin' version '1.8.0'
id "io.swagger.swaggerhub" version "1.3.2"
id 'edu.sc.seis.launch4j' version '3.0.5'
}
group = 'stirling.software'
version = '0.12.1'
version = '0.18.1'
sourceCompatibility = '17'
repositories {
mavenCentral()
}
sourceSets {
main {
java {
if (System.getenv('DOCKER_ENABLE_SECURITY') == 'false') {
exclude 'stirling/software/SPDF/config/security/**'
exclude 'stirling/software/SPDF/controller/api/UserController.java'
exclude 'stirling/software/SPDF/controller/web/AccountWebController.java'
exclude 'stirling/software/SPDF/model/ApiKeyAuthenticationToken.java'
exclude 'stirling/software/SPDF/model/Authority.java'
exclude 'stirling/software/SPDF/model/PersistentLogin.java'
exclude 'stirling/software/SPDF/model/User.java'
exclude 'stirling/software/SPDF/repository/**'
}
}
}
}
openApi {
apiDocsUrl = "http://localhost:8080/v3/api-docs"
apiDocsUrl = "http://localhost:8080/v1/api-docs"
outputDir = file("$projectDir")
outputFileName = "SwaggerDoc.json"
}
@@ -28,15 +45,15 @@ launch4j {
outfile="Stirling-PDF.exe"
headerType="console"
jarTask = tasks.bootJar
errTitle="Encountered error, Do you have Java 17?"
downloadUrl="https://download.oracle.com/java/17/latest/jdk-17_windows-x64_bin.exe"
variables=["BROWSER_OPEN=true"]
downloadUrl="https://download.oracle.com/java/17/latest/jdk-17_windows-x64_bin.exe"
variables=["BROWSER_OPEN=true", "ENDPOINTS_GROUPS_TO_REMOVE=CLI"]
jreMinVersion="17"
mutexName="Stirling-PDF"
windowTitle="Stirling-PDF"
messagesStartupError="An error occurred while starting Stirling-PDF"
//messagesJreNotFoundError="This application requires a Java Runtime Environment, Please download Java 17."
messagesJreVersionError="You are running the wrong version of Java, Please download Java 17."
@@ -45,30 +62,79 @@ launch4j {
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web:3.1.0'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf:3.1.1'
testImplementation 'org.springframework.boot:spring-boot-starter-test:3.1.0'
// https://mvnrepository.com/artifact/org.apache.pdfbox/jbig2-imageio
implementation group: 'org.apache.pdfbox', name: 'jbig2-imageio', version: '3.0.4'
implementation 'commons-io:commons-io:2.13.0'
//security updates
implementation 'ch.qos.logback:logback-classic:1.4.14'
implementation 'ch.qos.logback:logback-core:1.4.14'
implementation 'org.springframework:spring-webmvc:6.0.15'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.1.0'
implementation 'org.yaml:snakeyaml:2.1'
implementation 'org.springframework.boot:spring-boot-starter-web:3.2.1'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf:3.2.1'
if (System.getenv('DOCKER_ENABLE_SECURITY') != 'false') {
implementation 'org.springframework.boot:spring-boot-starter-security:3.2.1'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5:3.1.2.RELEASE'
implementation "org.springframework.boot:spring-boot-starter-data-jpa"
implementation "com.h2database:h2"
}
testImplementation 'org.springframework.boot:spring-boot-starter-test:3.2.1'
// Batik
implementation 'org.apache.xmlgraphics:batik-all:1.17'
// TwelveMonkeys
implementation 'com.twelvemonkeys.imageio:imageio-batik:3.10.1'
implementation 'com.twelvemonkeys.imageio:imageio-bmp:3.10.1'
// implementation 'com.twelvemonkeys.imageio:imageio-hdr:3.10.1'
// implementation 'com.twelvemonkeys.imageio:imageio-icns:3.10.1'
// implementation 'com.twelvemonkeys.imageio:imageio-iff:3.10.1'
implementation 'com.twelvemonkeys.imageio:imageio-jpeg:3.10.1'
// implementation 'com.twelvemonkeys.imageio:imageio-pcx:3.10.1'
// implementation 'com.twelvemonkeys.imageio:imageio-pict:3.10.1'
// implementation 'com.twelvemonkeys.imageio:imageio-pnm:3.10.1'
// implementation 'com.twelvemonkeys.imageio:imageio-psd:3.10.1'
// implementation 'com.twelvemonkeys.imageio:imageio-sgi:3.10.1'
// implementation 'com.twelvemonkeys.imageio:imageio-tga:3.10.1'
// implementation 'com.twelvemonkeys.imageio:imageio-thumbsdb:3.10.1'
implementation 'com.twelvemonkeys.imageio:imageio-tiff:3.10.1'
implementation 'com.twelvemonkeys.imageio:imageio-webp:3.10.1'
// implementation 'com.twelvemonkeys.imageio:imageio-xwd:3.10.1'
implementation 'commons-io:commons-io:2.15.1'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0'
//general PDF
// https://mvnrepository.com/artifact/com.opencsv/opencsv
implementation ('com.opencsv:opencsv:5.7.1') {
exclude group: 'commons-logging', module: 'commons-logging'
}
//general PDF
implementation 'org.apache.pdfbox:pdfbox:2.0.28'
implementation 'org.bouncycastle:bcprov-jdk15on:1.70'
implementation 'org.bouncycastle:bcpkix-jdk15on:1.70'
implementation 'com.itextpdf:itext7-core:7.2.5'
implementation ('org.apache.pdfbox:pdfbox:2.0.29'){
exclude group: 'commons-logging', module: 'commons-logging'
}
implementation ('org.apache.pdfbox:xmpbox:2.0.29'){
exclude group: 'commons-logging', module: 'commons-logging'
}
implementation 'org.bouncycastle:bcprov-jdk18on:1.77'
implementation 'org.bouncycastle:bcpkix-jdk18on:1.77'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'io.micrometer:micrometer-core'
implementation group: 'com.google.zxing', name: 'core', version: '3.5.1'
implementation group: 'com.google.zxing', name: 'core', version: '3.5.2'
// https://mvnrepository.com/artifact/org.commonmark/commonmark
implementation 'org.commonmark:commonmark:0.21.0'
developmentOnly("org.springframework.boot:spring-boot-devtools")
implementation 'org.commonmark:commonmark:0.21.0'
// https://mvnrepository.com/artifact/com.github.vladimir-bukhtoyarov/bucket4j-core
implementation 'com.github.vladimir-bukhtoyarov:bucket4j-core:7.6.0'
developmentOnly("org.springframework.boot:spring-boot-devtools")
compileOnly 'org.projectlombok:lombok:1.18.30'
annotationProcessor 'org.projectlombok:lombok:1.18.28'
}
task writeVersion {
def propsFile = file('src/main/resources/version.properties')
def props = new Properties()
@@ -94,7 +160,7 @@ jar {
attributes 'Implementation-Title': 'Stirling-PDF',
'Implementation-Version': project.version
}
}
tasks.named('test') {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

BIN
images/login-dark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

BIN
images/login-light.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 131 KiB

View File

@@ -0,0 +1,39 @@
{
"name": "Prepare-pdfs-for-email",
"pipeline": [
{
"operation": "/api/v1/misc/repair",
"parameters": {}
},
{
"operation": "/api/v1/security/sanitize-pdf",
"parameters": {
"removeJavaScript": true,
"removeEmbeddedFiles": false,
"removeMetadata": false,
"removeLinks": false,
"removeFonts": false
}
},
{
"operation": "/api/v1/misc/compress-pdf",
"parameters": {
"optimizeLevel": 2,
"expectedOutputSize": ""
}
},
{
"operation": "/api/v1/general/split-by-size-or-count",
"parameters": {
"splitType": 0,
"splitValue": "15MB"
}
}
],
"_examples": {
"outputDir": "{outputFolder}/{folderName}",
"outputFileName": "{filename}-{pipelineName}-{date}-{time}"
},
"outputDir": "httpWebRequest",
"outputFileName": "{filename}"
}

View File

@@ -0,0 +1,33 @@
{
"name": "split-rotate-auto-rename",
"pipeline": [
{
"operation": "/api/v1/general/split-pdf-by-sections",
"parameters": {
"horizontalDivisions": 2,
"verticalDivisions": 2,
"fileInput": "automated"
}
},
{
"operation": "/api/v1/general/rotate-pdf",
"parameters": {
"angle": 90,
"fileInput": "automated"
}
},
{
"operation": "/api/v1/misc/auto-rename",
"parameters": {
"useFirstTextAsFallback": false,
"fileInput": "automated"
}
}
],
"_examples": {
"outputDir": "{outputFolder}/{folderName}",
"outputFileName": "{filename}-{pipelineName}-{date}-{time}"
},
"outputDir": "{outputFolder}",
"outputFileName": "{filename}"
}

View File

@@ -1,11 +1,11 @@
import cv2
import numpy as np
import sys
import argparse
import numpy as np
def is_blank_image(image_path, threshold=10, white_percent=99, white_value=255, blur_size=5):
image = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
if image is None:
print(f"Error: Unable to read the image file: {image_path}")
return False
@@ -16,19 +16,11 @@ def is_blank_image(image_path, threshold=10, white_percent=99, white_value=255,
_, thresholded_image = cv2.threshold(blurred_image, white_value - threshold, white_value, cv2.THRESH_BINARY)
# Calculate the percentage of white pixels in the thresholded image
white_pixels = 0
total_pixels = thresholded_image.size
for i in range(0, thresholded_image.shape[0], 2):
for j in range(0, thresholded_image.shape[1], 2):
if thresholded_image[i, j] == white_value:
white_pixels += 1
white_pixel_percentage = (white_pixels / (i * thresholded_image.shape[1] + j + 1)) * 100
if white_pixel_percentage < white_percent:
return False
white_pixels = np.sum(thresholded_image == white_value)
white_pixel_percentage = (white_pixels / thresholded_image.size) * 100
print(f"Page has white pixel percent of {white_pixel_percentage}")
return True
return white_pixel_percentage >= white_percent
if __name__ == "__main__":
@@ -40,9 +32,6 @@ if __name__ == "__main__":
blank = is_blank_image(args.image_path, args.threshold, args.white_percent)
if blank:
# Return code 1: The image is considered blank.
sys.exit(1)
else:
# Return code 0: The image is not considered blank.
sys.exit(0)
# Return code 1: The image is considered blank.
# Return code 0: The image is not considered blank.
sys.exit(int(blank))

View File

@@ -0,0 +1,19 @@
echo "Running Stirling PDF with DOCKER_ENABLE_SECURITY=${DOCKER_ENABLE_SECURITY} and VERSION_TAG=${VERSION_TAG}"
# Check for DOCKER_ENABLE_SECURITY and download the appropriate JAR if required
if [ "$DOCKER_ENABLE_SECURITY" = "true" ] && [ "$VERSION_TAG" != "alpha" ]; then
if [ ! -f app-security.jar ]; then
echo "Trying to download from: https://github.com/Frooodle/Stirling-PDF/releases/download/v$VERSION_TAG/Stirling-PDF-with-login.jar"
curl -L -o app-security.jar https://github.com/Frooodle/Stirling-PDF/releases/download/v$VERSION_TAG/Stirling-PDF-with-login.jar
# If the first download attempt failed, try with the 'v' prefix
if [ $? -ne 0 ]; then
echo "Trying to download from: https://github.com/Frooodle/Stirling-PDF/releases/download/$VERSION_TAG/Stirling-PDF-with-login.jar"
curl -L -o app-security.jar https://github.com/Frooodle/Stirling-PDF/releases/download/$VERSION_TAG/Stirling-PDF-with-login.jar
fi
if [ $? -eq 0 ]; then # checks if curl was successful
rm -f app.jar
ln -s app-security.jar app.jar
fi
fi
fi

View File

@@ -0,0 +1,6 @@
#!/bin/sh
/scripts/download-security-jar.sh
# Run the main command
exec "$@"

View File

@@ -3,7 +3,11 @@
# Copy the original tesseract-ocr files to the volume directory without overwriting existing files
echo "Copying original files without overwriting existing files"
mkdir -p /usr/share/tesseract-ocr
cp -rn /usr/share/tesseract-ocr-original/* /usr/share/tesseract-ocr
cp -rn /usr/share/tesseract-ocr-original/* /usr/share/tesseract-ocr
if [ -d /usr/share/tesseract-ocr/4.00/tessdata ]; then
cp -r /usr/share/tesseract-ocr/4.00/tessdata/* /usr/share/tesseract-ocr/5/tessdata/ || true;
fi
# Check if TESSERACT_LANGS environment variable is set and is not empty
if [[ -n "$TESSERACT_LANGS" ]]; then
@@ -16,6 +20,7 @@ if [[ -n "$TESSERACT_LANGS" ]]; then
done
fi
/scripts/download-security-jar.sh
# Run the main command
exec "$@"

View File

@@ -1,9 +1,8 @@
package stirling.software.SPDF;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
@@ -12,12 +11,13 @@ import org.springframework.core.env.Environment;
import org.springframework.scheduling.annotation.EnableScheduling;
import jakarta.annotation.PostConstruct;
import stirling.software.SPDF.config.ConfigInitializer;
import stirling.software.SPDF.utils.GeneralUtils;
@SpringBootApplication
//@EnableScheduling
@EnableScheduling
public class SPdfApplication {
@Autowired
private Environment env;
@@ -29,11 +29,7 @@ public class SPdfApplication {
if (browserOpen) {
try {
String port = env.getProperty("local.server.port");
if(port == null || port.length() == 0) {
port="8080";
}
String url = "http://localhost:" + port;
String url = "http://localhost:" + getPort();
String os = System.getProperty("os.name").toLowerCase();
Runtime rt = Runtime.getRuntime();
@@ -46,31 +42,38 @@ public class SPdfApplication {
}
}
}
public static void main(String[] args) {
SpringApplication.run(SPdfApplication.class, args);
SpringApplication app = new SpringApplication(SPdfApplication.class);
app.addInitializers(new ConfigInitializer());
if (Files.exists(Paths.get("configs/settings.yml"))) {
app.setDefaultProperties(Collections.singletonMap("spring.config.additional-location", "file:configs/settings.yml"));
} else {
System.out.println("External configuration file 'configs/settings.yml' does not exist. Using default configuration and environment configuration instead.");
}
app.run(args);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
GeneralUtils.createDir("customFiles/static/");
GeneralUtils.createDir("customFiles/templates/");
GeneralUtils.createDir("config");
System.out.println("Stirling-PDF Started.");
String port = System.getProperty("local.server.port");
if(port == null || port.length() == 0) {
port="8080";
}
String url = "http://localhost:" + port;
String url = "http://localhost:" + getPort();
System.out.println("Navigate to " + url);
}
}
public static String getPort() {
String port = System.getProperty("local.server.port");
if (port == null || port.isEmpty()) {
port = "8080";
}
return port;
}
}

View File

@@ -1,42 +1,58 @@
package stirling.software.SPDF.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AppConfig {
@Bean(name = "appName")
public String appName() {
String appName = System.getProperty("APP_HOME_NAME");
if (appName == null)
appName = System.getenv("APP_HOME_NAME");
return (appName != null) ? appName : "Stirling PDF";
}
@Bean(name = "appVersion")
public String appVersion() {
String version = getClass().getPackage().getImplementationVersion();
return (version != null) ? version : "0.0.0";
}
@Bean(name = "homeText")
public String homeText() {
String homeText = System.getProperty("APP_HOME_DESCRIPTION");
if (homeText == null)
homeText = System.getenv("APP_HOME_DESCRIPTION");
return (homeText != null) ? homeText : "null";
}
@Bean(name = "navBarText")
public String navBarText() {
String navBarText = System.getProperty("APP_NAVBAR_NAME");
if (navBarText == null)
navBarText = System.getenv("APP_NAVBAR_NAME");
if (navBarText == null)
navBarText = System.getProperty("APP_HOME_NAME");
if (navBarText == null)
navBarText = System.getenv("APP_HOME_NAME");
return (navBarText != null) ? navBarText : "Stirling PDF";
}
package stirling.software.SPDF.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import stirling.software.SPDF.model.ApplicationProperties;
@Configuration
public class AppConfig {
@Autowired
ApplicationProperties applicationProperties;
@Bean(name = "loginEnabled")
public boolean loginEnabled() {
return applicationProperties.getSecurity().getEnableLogin();
}
@Bean(name = "appName")
public String appName() {
String homeTitle = applicationProperties.getUi().getAppName();
return (homeTitle != null) ? homeTitle : "Stirling PDF";
}
@Bean(name = "appVersion")
public String appVersion() {
String version = getClass().getPackage().getImplementationVersion();
return (version != null) ? version : "0.0.0";
}
@Bean(name = "homeText")
public String homeText() {
return (applicationProperties.getUi().getHomeDescription() != null) ? applicationProperties.getUi().getHomeDescription() : "null";
}
@Bean(name = "navBarText")
public String navBarText() {
String defaultNavBar = applicationProperties.getUi().getAppNameNavbar() != null ? applicationProperties.getUi().getAppNameNavbar() : applicationProperties.getUi().getAppName();
return (defaultNavBar != null) ? defaultNavBar : "Stirling PDF";
}
@Bean(name = "enableAlphaFunctionality")
public boolean enableAlphaFunctionality() {
return applicationProperties.getSystem().getEnableAlphaFunctionality() != null ? applicationProperties.getSystem().getEnableAlphaFunctionality() : false;
}
@Bean(name = "rateLimit")
public boolean rateLimit() {
String appName = System.getProperty("rateLimit");
if (appName == null)
appName = System.getenv("rateLimit");
return (appName != null) ? Boolean.valueOf(appName) : false;
}
}

View File

@@ -2,6 +2,7 @@ package stirling.software.SPDF.config;
import java.util.Locale;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.LocaleResolver;
@@ -10,9 +11,14 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
import org.springframework.web.servlet.i18n.SessionLocaleResolver;
import stirling.software.SPDF.model.ApplicationProperties;
@Configuration
public class Beans implements WebMvcConfigurer {
@Autowired
ApplicationProperties applicationProperties;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(localeChangeInterceptor());
@@ -29,10 +35,9 @@ public class Beans implements WebMvcConfigurer {
@Bean
public LocaleResolver localeResolver() {
SessionLocaleResolver slr = new SessionLocaleResolver();
String appLocaleEnv = System.getProperty("APP_LOCALE");
if (appLocaleEnv == null)
appLocaleEnv = System.getenv("APP_LOCALE");
String appLocaleEnv = applicationProperties.getSystem().getDefaultLocale();
Locale defaultLocale = Locale.UK; // Fallback to UK locale if environment variable is not set
if (appLocaleEnv != null && !appLocaleEnv.isEmpty()) {

View File

@@ -1,79 +1,68 @@
package stirling.software.SPDF.config;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.Arrays;
import java.util.List;
import java.util.HashMap;
import java.util.Map;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
public class CleanUrlInterceptor implements HandlerInterceptor {
private static final List<String> ALLOWED_PARAMS = Arrays.asList("lang", "endpoint", "endpoints");
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
String queryString = request.getQueryString();
if (queryString != null && !queryString.isEmpty()) {
String requestURI = request.getRequestURI();
Map<String, String> parameters = new HashMap<>();
// Keep only the allowed parameters
String[] queryParameters = queryString.split("&");
for (String param : queryParameters) {
String[] keyValue = param.split("=");
if (keyValue.length != 2) {
continue;
}
if (ALLOWED_PARAMS.contains(keyValue[0])) {
parameters.put(keyValue[0], keyValue[1]);
}
}
// If there are any parameters that are not allowed
if (parameters.size() != queryParameters.length) {
// Construct new query string
StringBuilder newQueryString = new StringBuilder();
for (Map.Entry<String, String> entry : parameters.entrySet()) {
if (newQueryString.length() > 0) {
newQueryString.append("&");
}
newQueryString.append(entry.getKey()).append("=").append(entry.getValue());
}
// Redirect to the URL with only allowed query parameters
String redirectUrl = requestURI + "?" + newQueryString;
response.sendRedirect(redirectUrl);
return false;
}
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
Exception ex) {
}
}
package stirling.software.SPDF.config;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
public class CleanUrlInterceptor implements HandlerInterceptor {
private static final List<String> ALLOWED_PARAMS = Arrays.asList("lang", "endpoint", "endpoints", "logout", "error", "file", "messageType");
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
String queryString = request.getQueryString();
if (queryString != null && !queryString.isEmpty()) {
String requestURI = request.getRequestURI();
Map<String, String> parameters = new HashMap<>();
// Keep only the allowed parameters
String[] queryParameters = queryString.split("&");
for (String param : queryParameters) {
String[] keyValue = param.split("=");
if (keyValue.length != 2) {
continue;
}
if (ALLOWED_PARAMS.contains(keyValue[0])) {
parameters.put(keyValue[0], keyValue[1]);
}
}
// If there are any parameters that are not allowed
if (parameters.size() != queryParameters.length) {
// Construct new query string
StringBuilder newQueryString = new StringBuilder();
for (Map.Entry<String, String> entry : parameters.entrySet()) {
if (newQueryString.length() > 0) {
newQueryString.append("&");
}
newQueryString.append(entry.getKey()).append("=").append(entry.getValue());
}
// Redirect to the URL with only allowed query parameters
String redirectUrl = requestURI + "?" + newQueryString;
response.sendRedirect(redirectUrl);
return false;
}
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
Exception ex) {
}
}

View File

@@ -0,0 +1,129 @@
package stirling.software.SPDF.config;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
public class ConfigInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
try {
ensureConfigExists();
} catch (IOException e) {
throw new RuntimeException("Failed to initialize application configuration", e);
}
}
public void ensureConfigExists() throws IOException {
// Define the path to the external config directory
Path destPath = Paths.get("configs", "settings.yml");
// Check if the file already exists
if (Files.notExists(destPath)) {
// Ensure the destination directory exists
Files.createDirectories(destPath.getParent());
// Copy the resource from classpath to the external directory
try (InputStream in = getClass().getClassLoader().getResourceAsStream("settings.yml.template")) {
if (in != null) {
Files.copy(in, destPath);
} else {
throw new FileNotFoundException("Resource file not found: settings.yml.template");
}
}
} else {
// If user file exists, we need to merge it with the template from the classpath
List<String> templateLines;
try (InputStream in = getClass().getClassLoader().getResourceAsStream("settings.yml.template")) {
templateLines = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8)).lines()
.collect(Collectors.toList());
}
mergeYamlFiles(templateLines, destPath, destPath);
}
}
public void mergeYamlFiles(List<String> templateLines, Path userFilePath, Path outputPath) throws IOException {
List<String> userLines = Files.readAllLines(userFilePath);
List<String> mergedLines = new ArrayList<>();
boolean insideAutoGenerated = false;
boolean beforeFirstKey = true;
Function<String, Boolean> isCommented = line -> line.trim().startsWith("#");
Function<String, String> extractKey = line -> {
String[] parts = line.split(":");
return parts.length > 0 ? parts[0].trim().replace("#", "").trim() : "";
};
Set<String> userKeys = userLines.stream().map(extractKey).collect(Collectors.toSet());
for (String line : templateLines) {
String key = extractKey.apply(line);
if (line.trim().equalsIgnoreCase("AutomaticallyGenerated:")) {
insideAutoGenerated = true;
mergedLines.add(line);
continue;
} else if (insideAutoGenerated && line.trim().isEmpty()) {
insideAutoGenerated = false;
mergedLines.add(line);
continue;
}
if (beforeFirstKey && (isCommented.apply(line) || line.trim().isEmpty())) {
// Handle top comments and empty lines before the first key.
mergedLines.add(line);
continue;
}
if (!key.isEmpty())
beforeFirstKey = false;
if (userKeys.contains(key)) {
// If user has any version (commented or uncommented) of this key, skip the
// template line
Optional<String> userValue = userLines.stream()
.filter(l -> extractKey.apply(l).equalsIgnoreCase(key) && !isCommented.apply(l)).findFirst();
if (userValue.isPresent())
mergedLines.add(userValue.get());
continue;
}
if (isCommented.apply(line) || line.trim().isEmpty() || !userKeys.contains(key)) {
mergedLines.add(line); // If line is commented, empty or key not present in user's file, retain the
// template line
continue;
}
}
// Add any additional uncommented user lines that are not present in the
// template
for (String userLine : userLines) {
String userKey = extractKey.apply(userLine);
boolean isPresentInTemplate = templateLines.stream().map(extractKey)
.anyMatch(templateKey -> templateKey.equalsIgnoreCase(userKey));
if (!isPresentInTemplate && !isCommented.apply(userLine)) {
mergedLines.add(userLine);
}
}
Files.write(outputPath, mergedLines, StandardCharsets.UTF_8);
}
}

View File

@@ -1,20 +1,28 @@
package stirling.software.SPDF.config;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import stirling.software.SPDF.model.ApplicationProperties;
@Service
public class EndpointConfiguration {
private static final Logger logger = LoggerFactory.getLogger(EndpointConfiguration.class);
private Map<String, Boolean> endpointStatuses = new ConcurrentHashMap<>();
private Map<String, Set<String>> endpointGroups = new ConcurrentHashMap<>();
public EndpointConfiguration() {
private final ApplicationProperties applicationProperties;
@Autowired
public EndpointConfiguration(ApplicationProperties applicationProperties) {
this.applicationProperties = applicationProperties;
init();
processEnvironmentConfigs();
}
@@ -73,6 +81,10 @@ public class EndpointConfiguration {
addEndpointToGroup("PageOps", "auto-split-pdf");
addEndpointToGroup("PageOps", "extract-page");
addEndpointToGroup("PageOps", "pdf-to-single-page");
addEndpointToGroup("PageOps", "split-by-size-or-count");
addEndpointToGroup("PageOps", "overlay-pdf");
addEndpointToGroup("PageOps", "split-pdf-by-sections");
// Adding endpoints to "Convert" group
addEndpointToGroup("Convert", "pdf-to-img");
@@ -88,6 +100,8 @@ public class EndpointConfiguration {
addEndpointToGroup("Convert", "html-to-pdf");
addEndpointToGroup("Convert", "url-to-pdf");
addEndpointToGroup("Convert", "markdown-to-pdf");
addEndpointToGroup("Convert", "pdf-to-csv");
// Adding endpoints to "Security" group
addEndpointToGroup("Security", "add-password");
@@ -96,6 +110,7 @@ public class EndpointConfiguration {
addEndpointToGroup("Security", "add-watermark");
addEndpointToGroup("Security", "cert-sign");
addEndpointToGroup("Security", "sanitize-pdf");
addEndpointToGroup("Security", "auto-redact");
// Adding endpoints to "Other" group
@@ -109,6 +124,7 @@ public class EndpointConfiguration {
addEndpointToGroup("Other", "flatten");
addEndpointToGroup("Other", "repair");
addEndpointToGroup("Other", "remove-blanks");
addEndpointToGroup("Other", "remove-annotations");
addEndpointToGroup("Other", "compare");
addEndpointToGroup("Other", "add-page-numbers");
addEndpointToGroup("Other", "auto-rename");
@@ -189,6 +205,11 @@ public class EndpointConfiguration {
addEndpointToGroup("Java", "pdf-to-single-page");
addEndpointToGroup("Java", "markdown-to-pdf");
addEndpointToGroup("Java", "show-javascript");
addEndpointToGroup("Java", "auto-redact");
addEndpointToGroup("Java", "pdf-to-csv");
addEndpointToGroup("Java", "split-by-size-or-count");
addEndpointToGroup("Java", "overlay-pdf");
addEndpointToGroup("Java", "split-pdf-by-sections");
//Javascript
addEndpointToGroup("Javascript", "pdf-organizer");
@@ -198,21 +219,19 @@ public class EndpointConfiguration {
}
private void processEnvironmentConfigs() {
String endpointsToRemove = System.getenv("ENDPOINTS_TO_REMOVE");
String groupsToRemove = System.getenv("GROUPS_TO_REMOVE");
List<String> endpointsToRemove = applicationProperties.getEndpoints().getToRemove();
List<String> groupsToRemove = applicationProperties.getEndpoints().getGroupsToRemove();
if (endpointsToRemove != null) {
String[] endpoints = endpointsToRemove.split(",");
for (String endpoint : endpoints) {
for (String endpoint : endpointsToRemove) {
disableEndpoint(endpoint.trim());
}
}
if (groupsToRemove != null) {
String[] groups = groupsToRemove.split(",");
for (String group : groups) {
for (String group : groupsToRemove) {
disableGroup(group.trim());
}
}

View File

@@ -1,26 +1,26 @@
package stirling.software.SPDF.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@Component
public class EndpointInterceptor implements HandlerInterceptor {
@Autowired
private EndpointConfiguration endpointConfiguration;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
String requestURI = request.getRequestURI();
if (!endpointConfiguration.isEndpointEnabled(requestURI)) {
response.sendError(HttpServletResponse.SC_FORBIDDEN, "This endpoint is disabled");
return false;
}
return true;
}
package stirling.software.SPDF.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@Component
public class EndpointInterceptor implements HandlerInterceptor {
@Autowired
private EndpointConfiguration endpointConfiguration;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
String requestURI = request.getRequestURI();
if (!endpointConfiguration.isEndpointEnabled(requestURI)) {
response.sendError(HttpServletResponse.SC_FORBIDDEN, "This endpoint is disabled");
return false;
}
return true;
}
}

View File

@@ -1,24 +1,24 @@
package stirling.software.SPDF.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import io.micrometer.core.instrument.Meter;
import io.micrometer.core.instrument.config.MeterFilter;
import io.micrometer.core.instrument.config.MeterFilterReply;
@Configuration
public class MetricsConfig {
@Bean
public MeterFilter meterFilter() {
return new MeterFilter() {
@Override
public MeterFilterReply accept(Meter.Id id) {
if (id.getName().equals("http.requests")) {
return MeterFilterReply.NEUTRAL;
}
return MeterFilterReply.DENY;
}
};
}
package stirling.software.SPDF.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import io.micrometer.core.instrument.Meter;
import io.micrometer.core.instrument.config.MeterFilter;
import io.micrometer.core.instrument.config.MeterFilterReply;
@Configuration
public class MetricsConfig {
@Bean
public MeterFilter meterFilter() {
return new MeterFilter() {
@Override
public MeterFilterReply accept(Meter.Id id) {
if (id.getName().equals("http.requests")) {
return MeterFilterReply.NEUTRAL;
}
return MeterFilterReply.DENY;
}
};
}
}

View File

@@ -1,48 +1,50 @@
package stirling.software.SPDF.config;
import java.io.IOException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@Component
public class MetricsFilter extends OncePerRequestFilter {
private final MeterRegistry meterRegistry;
@Autowired
public MetricsFilter(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String uri = request.getRequestURI();
//System.out.println("uri="+uri + ", method=" + request.getMethod() );
// Ignore static resources
if (!(uri.startsWith("/js") || uri.startsWith("/images") || uri.endsWith(".ico") || uri.endsWith(".css") || uri.endsWith(".svg")|| uri.endsWith(".js") || uri.contains("swagger") || uri.startsWith("/api"))) {
Counter counter = Counter.builder("http.requests")
.tag("uri", uri)
.tag("method", request.getMethod())
.register(meterRegistry);
counter.increment();
//System.out.println("Counted");
}
filterChain.doFilter(request, response);
}
}
package stirling.software.SPDF.config;
import java.io.IOException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@Component
public class MetricsFilter extends OncePerRequestFilter {
private final MeterRegistry meterRegistry;
@Autowired
public MetricsFilter(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String uri = request.getRequestURI();
// System.out.println("uri="+uri + ", method=" + request.getMethod() );
// Ignore static resources
if (!(uri.startsWith("/js") || uri.startsWith("/v1/api-docs") || uri.endsWith("robots.txt")
|| uri.startsWith("/images") || uri.startsWith("/images")|| uri.endsWith(".png") || uri.endsWith(".ico") || uri.endsWith(".css") || uri.endsWith(".map")
|| uri.endsWith(".svg") || uri.endsWith(".js") || uri.contains("swagger")
|| uri.startsWith("/api/v1/info") || uri.startsWith("/site.webmanifest") || uri.startsWith("/fonts") || uri.startsWith("/pdfjs") )) {
Counter counter = Counter.builder("http.requests").tag("uri", uri).tag("method", request.getMethod())
.register(meterRegistry);
counter.increment();
// System.out.println("Counted");
}
filterChain.doFilter(request, response);
}
}

View File

@@ -1,36 +1,42 @@
package stirling.software.SPDF.config;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
@Configuration
public class OpenApiConfig {
@Bean
public OpenAPI customOpenAPI() {
String version = getClass().getPackage().getImplementationVersion();
if (version == null) {
Properties props = new Properties();
try (InputStream input = getClass().getClassLoader().getResourceAsStream("version.properties")) {
props.load(input);
version = props.getProperty("version");
} catch (IOException ex) {
ex.printStackTrace();
version = "1.0.0"; // default version if all else fails
}
}
return new OpenAPI().components(new Components()).info(
new Info().title("Stirling PDF API").version(version).description("API documentation for all Server-Side processing.\nPlease note some functionality might be UI only and missing from here."));
}
}
package stirling.software.SPDF.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import stirling.software.SPDF.model.ApplicationProperties;
@Configuration
public class OpenApiConfig {
@Autowired
ApplicationProperties applicationProperties;
@Bean
public OpenAPI customOpenAPI() {
String version = getClass().getPackage().getImplementationVersion();
if (version == null) {
version = "1.0.0"; // default version if all else fails
}
SecurityScheme apiKeyScheme = new SecurityScheme().type(SecurityScheme.Type.APIKEY).in(SecurityScheme.In.HEADER)
.name("X-API-KEY");
if (!applicationProperties.getSecurity().getEnableLogin()) {
return new OpenAPI().components(new Components())
.info(new Info().title("Stirling PDF API").version(version).description(
"API documentation for all Server-Side processing.\nPlease note some functionality might be UI only and missing from here."));
} else {
return new OpenAPI().components(new Components().addSecuritySchemes("apiKey", apiKeyScheme))
.info(new Info().title("Stirling PDF API").version(version).description(
"API documentation for all Server-Side processing.\nPlease note some functionality might be UI only and missing from here."))
.addSecurityItem(new SecurityRequirement().addList("apiKey"));
}
}
}

View File

@@ -0,0 +1,20 @@
package stirling.software.SPDF.config;
import java.time.LocalDateTime;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.stereotype.Component;
@Component
public class StartupApplicationListener implements ApplicationListener<ContextRefreshedEvent> {
public static LocalDateTime startTime;
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
startTime = LocalDateTime.now();
}
}

View File

@@ -1,27 +1,27 @@
package stirling.software.SPDF.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private EndpointInterceptor endpointInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(endpointInterceptor);
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// Handler for external static resources
registry.addResourceHandler("/**")
.addResourceLocations("file:customFiles/static/", "classpath:/static/")
.setCachePeriod(0); // Optional: disable caching
}
}
package stirling.software.SPDF.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private EndpointInterceptor endpointInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(endpointInterceptor);
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// Handler for external static resources
registry.addResourceHandler("/**")
.addResourceLocations("file:customFiles/static/", "classpath:/static/");
//.setCachePeriod(0); // Optional: disable caching
}
}

View File

@@ -0,0 +1,23 @@
package stirling.software.SPDF.config;
import java.io.IOException;
import java.util.Properties;
import org.springframework.beans.factory.config.YamlPropertiesFactoryBean;
import org.springframework.core.env.PropertiesPropertySource;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.support.EncodedResource;
import org.springframework.core.io.support.PropertySourceFactory;
public class YamlPropertySourceFactory implements PropertySourceFactory {
@Override
public PropertySource<?> createPropertySource(String name, EncodedResource encodedResource)
throws IOException {
YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
factory.setResources(encodedResource.getResource());
Properties properties = factory.getObject();
return new PropertiesPropertySource(encodedResource.getResource().getFilename(), properties);
}
}

View File

@@ -0,0 +1,47 @@
package stirling.software.SPDF.config.security;
import java.io.IOException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@Component
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
@Autowired
private final LoginAttemptService loginAttemptService;
@Autowired
public CustomAuthenticationFailureHandler(LoginAttemptService loginAttemptService) {
this.loginAttemptService = loginAttemptService;
}
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception)
throws IOException, ServletException {
String ip = request.getRemoteAddr();
logger.error("Failed login attempt from IP: " + ip);
String username = request.getParameter("username");
if(loginAttemptService.loginAttemptCheck(username)) {
setDefaultFailureUrl("/login?error=locked");
} else {
if (exception.getClass().isAssignableFrom(BadCredentialsException.class)) {
setDefaultFailureUrl("/login?error=badcredentials");
} else if (exception.getClass().isAssignableFrom(LockedException.class)) {
setDefaultFailureUrl("/login?error=locked");
}
}
super.onAuthenticationFailure(request, response, exception);
}
}

View File

@@ -0,0 +1,44 @@
package stirling.software.SPDF.config.security;
import java.io.IOException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.security.web.savedrequest.SavedRequest;
import org.springframework.stereotype.Component;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import stirling.software.SPDF.utils.RequestUriUtils;
@Component
public class CustomAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
@Autowired
private LoginAttemptService loginAttemptService;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
String username = request.getParameter("username");
loginAttemptService.loginSucceeded(username);
// Get the saved request
HttpSession session = request.getSession(false);
SavedRequest savedRequest = session != null ? (SavedRequest) session.getAttribute("SPRING_SECURITY_SAVED_REQUEST") : null;
if (savedRequest != null && !RequestUriUtils.isStaticResource(savedRequest.getRedirectUrl())) {
// Redirect to the original destination
super.onAuthenticationSuccess(request, response, authentication);
} else {
// Redirect to the root URL (considering context path)
getRedirectStrategy().sendRedirect(request, response, "/");
}
//super.onAuthenticationSuccess(request, response, authentication);
}
}

View File

@@ -0,0 +1,52 @@
package stirling.software.SPDF.config.security;
import java.util.Collection;
import java.util.Set;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import stirling.software.SPDF.model.Authority;
import stirling.software.SPDF.model.User;
import stirling.software.SPDF.repository.UserRepository;
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Autowired
private LoginAttemptService loginAttemptService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("No user found with username: " + username));
if (loginAttemptService.isBlocked(username)) {
throw new LockedException("Your account has been locked due to too many failed login attempts.");
}
return new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPassword(),
user.isEnabled(),
true, true, true,
getAuthorities(user.getAuthorities())
);
}
private Collection<? extends GrantedAuthority> getAuthorities(Set<Authority> authorities) {
return authorities.stream()
.map(authority -> new SimpleGrantedAuthority(authority.getAuthority()))
.collect(Collectors.toList());
}
}

View File

@@ -0,0 +1,50 @@
package stirling.software.SPDF.config.security;
import java.io.IOException;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import stirling.software.SPDF.model.User;
import stirling.software.SPDF.utils.RequestUriUtils;
@Component
public class FirstLoginFilter extends OncePerRequestFilter {
@Autowired
@Lazy
private UserService userService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String method = request.getMethod();
String requestURI = request.getRequestURI();
// Check if the request is for static resources
boolean isStaticResource = RequestUriUtils.isStaticResource(requestURI);
// If it's a static resource, just continue the filter chain and skip the logic below
if (isStaticResource) {
filterChain.doFilter(request, response);
return;
}
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.isAuthenticated()) {
Optional<User> user = userService.findByUsername(authentication.getName());
if ("GET".equalsIgnoreCase(method) && user.isPresent() && user.get().isFirstLogin() && !"/change-creds".equals(requestURI)) {
response.sendRedirect("/change-creds");
return;
}
}
filterChain.doFilter(request, response);
}
}

View File

@@ -0,0 +1,65 @@
package stirling.software.SPDF.config.security;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import stirling.software.SPDF.utils.RequestUriUtils;
public class IPRateLimitingFilter implements Filter {
private final ConcurrentHashMap<String, AtomicInteger> requestCounts = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, AtomicInteger> getCounts = new ConcurrentHashMap<>();
private final int maxRequests;
private final int maxGetRequests;
public IPRateLimitingFilter(int maxRequests, int maxGetRequests) {
this.maxRequests = maxRequests;
this.maxGetRequests = maxGetRequests;
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
if (request instanceof HttpServletRequest) {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String method = httpRequest.getMethod();
String requestURI = httpRequest.getRequestURI();
// Check if the request is for static resources
boolean isStaticResource = RequestUriUtils.isStaticResource(requestURI);
// If it's a static resource, just continue the filter chain and skip the logic below
if (isStaticResource) {
chain.doFilter(request, response);
return;
}
String clientIp = request.getRemoteAddr();
requestCounts.computeIfAbsent(clientIp, k -> new AtomicInteger(0));
if (!"GET".equalsIgnoreCase(method)) {
if (requestCounts.get(clientIp).incrementAndGet() > maxRequests) {
// Handle limit exceeded (e.g., send error response)
response.getWriter().write("Rate limit exceeded");
return;
}
} else {
if (requestCounts.get(clientIp).incrementAndGet() > maxGetRequests) {
// Handle limit exceeded (e.g., send error response)
response.getWriter().write("GET Rate limit exceeded");
return;
}
}
}
chain.doFilter(request, response);
}
public void resetRequestCounts() {
requestCounts.clear();
getCounts.clear();
}
}

View File

@@ -0,0 +1,87 @@
package stirling.software.SPDF.config.security;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.UUID;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import jakarta.annotation.PostConstruct;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.Role;
@Component
public class InitialSecuritySetup {
@Autowired
private UserService userService;
@Autowired
ApplicationProperties applicationProperties;
@PostConstruct
public void init() {
if (!userService.hasUsers()) {
String initialUsername = applicationProperties.getSecurity().getInitialLogin().getUsername();
String initialPassword = applicationProperties.getSecurity().getInitialLogin().getPassword();
if (initialUsername != null && initialPassword != null) {
userService.saveUser(initialUsername, initialPassword, Role.ADMIN.getRoleId());
} else {
initialUsername = "admin";
initialPassword = "stirling";
userService.saveUser(initialUsername, initialPassword, Role.ADMIN.getRoleId(), true);
}
}
if(!userService.usernameExists(Role.INTERNAL_API_USER.getRoleId())) {
userService.saveUser(Role.INTERNAL_API_USER.getRoleId(), UUID.randomUUID().toString(), Role.INTERNAL_API_USER.getRoleId());
userService.addApiKeyToUser(Role.INTERNAL_API_USER.getRoleId());
}
}
@PostConstruct
public void initSecretKey() throws IOException {
String secretKey = applicationProperties.getAutomaticallyGenerated().getKey();
if (secretKey == null || secretKey.isEmpty()) {
secretKey = UUID.randomUUID().toString(); // Generating a random UUID as the secret key
saveKeyToConfig(secretKey);
}
}
private void saveKeyToConfig(String key) throws IOException {
Path path = Paths.get("configs", "settings.yml"); // Target the configs/settings.yml
List<String> lines = Files.readAllLines(path);
boolean keyFound = false;
// Search for the existing key to replace it or place to add it
for (int i = 0; i < lines.size(); i++) {
if (lines.get(i).startsWith("AutomaticallyGenerated:")) {
keyFound = true;
if (i + 1 < lines.size() && lines.get(i + 1).trim().startsWith("key:")) {
lines.set(i + 1, " key: " + key);
break;
} else {
lines.add(i + 1, " key: " + key);
break;
}
}
}
// If the section doesn't exist, append it
if (!keyFound) {
lines.add("# Automatically Generated Settings (Do Not Edit Directly)");
lines.add("AutomaticallyGenerated:");
lines.add(" key: " + key);
}
// Write back to the file
Files.write(path, lines);
}
}

View File

@@ -0,0 +1,56 @@
package stirling.software.SPDF.config.security;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import jakarta.annotation.PostConstruct;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.AttemptCounter;
@Service
public class LoginAttemptService {
@Autowired
ApplicationProperties applicationProperties;
private int MAX_ATTEMPTS;
private long ATTEMPT_INCREMENT_TIME;
@PostConstruct
public void init() {
MAX_ATTEMPTS = applicationProperties.getSecurity().getLoginAttemptCount();
ATTEMPT_INCREMENT_TIME = TimeUnit.MINUTES.toMillis(applicationProperties.getSecurity().getLoginResetTimeMinutes());
}
private final ConcurrentHashMap<String, AttemptCounter> attemptsCache = new ConcurrentHashMap<>();
public void loginSucceeded(String key) {
attemptsCache.remove(key);
}
public boolean loginAttemptCheck(String key) {
attemptsCache.compute(key, (k, attemptCounter) -> {
if (attemptCounter == null || attemptCounter.shouldReset(ATTEMPT_INCREMENT_TIME)) {
return new AttemptCounter();
} else {
attemptCounter.increment();
return attemptCounter;
}
});
return attemptsCache.get(key).getAttemptCount() >= MAX_ATTEMPTS;
}
public boolean isBlocked(String key) {
AttemptCounter attemptCounter = attemptsCache.get(key);
if (attemptCounter != null) {
return attemptCounter.getAttemptCount() >= MAX_ATTEMPTS;
}
return false;
}
}

View File

@@ -0,0 +1,18 @@
package stirling.software.SPDF.config.security;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Component
public class RateLimitResetScheduler {
private final IPRateLimitingFilter rateLimitingFilter;
public RateLimitResetScheduler(IPRateLimitingFilter rateLimitingFilter) {
this.rateLimitingFilter = rateLimitingFilter;
}
@Scheduled(cron = "0 0 0 * * MON") // At 00:00 every Monday TODO: configurable
public void resetRateLimit() {
rateLimitingFilter.resetRequestCounts();
}
}

View File

@@ -0,0 +1,136 @@
package stirling.software.SPDF.config.security;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.security.web.savedrequest.NullRequestCache;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import stirling.software.SPDF.repository.JPATokenRepositoryImpl;
@Configuration
@EnableWebSecurity()
@EnableMethodSecurity
public class SecurityConfiguration {
@Autowired
private UserDetailsService userDetailsService;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Autowired
@Lazy
private UserService userService;
@Autowired
@Qualifier("loginEnabled")
public boolean loginEnabledValue;
@Autowired
private UserAuthenticationFilter userAuthenticationFilter;
@Autowired
private LoginAttemptService loginAttemptService;
@Autowired
private FirstLoginFilter firstLoginFilter;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.addFilterBefore(userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
if(loginEnabledValue) {
http.csrf(csrf -> csrf.disable());
http.addFilterBefore(rateLimitingFilter(), UsernamePasswordAuthenticationFilter.class);
http.addFilterAfter(firstLoginFilter, UsernamePasswordAuthenticationFilter.class);
http
.formLogin(formLogin -> formLogin
.loginPage("/login")
.successHandler(new CustomAuthenticationSuccessHandler())
.defaultSuccessUrl("/")
.failureHandler(new CustomAuthenticationFailureHandler(loginAttemptService))
.permitAll()
).requestCache(requestCache -> requestCache
.requestCache(new NullRequestCache())
)
.logout(logout -> logout
.logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
.logoutSuccessUrl("/login?logout=true")
.invalidateHttpSession(true) // Invalidate session
.deleteCookies("JSESSIONID", "remember-me")
).rememberMe(rememberMeConfigurer -> rememberMeConfigurer // Use the configurator directly
.key("uniqueAndSecret")
.tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(1209600) // 2 weeks
)
.authorizeHttpRequests(authz -> authz
.requestMatchers(req -> {
String uri = req.getRequestURI();
String contextPath = req.getContextPath();
// Remove the context path from the URI
String trimmedUri = uri.startsWith(contextPath) ? uri.substring(contextPath.length()) : uri;
return trimmedUri.startsWith("/login") || trimmedUri.endsWith(".svg") ||
trimmedUri.startsWith("/register") || trimmedUri.startsWith("/error") ||
trimmedUri.startsWith("/images/") || trimmedUri.startsWith("/public/") ||
trimmedUri.startsWith("/css/") || trimmedUri.startsWith("/js/");
}
).permitAll()
.anyRequest().authenticated()
)
.userDetailsService(userDetailsService)
.authenticationProvider(authenticationProvider());
} else {
http.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(authz -> authz
.anyRequest().permitAll()
);
}
return http.build();
}
@Bean
public IPRateLimitingFilter rateLimitingFilter() {
int maxRequestsPerIp = 1000000; // Example limit TODO add config level
return new IPRateLimitingFilter(maxRequestsPerIp, maxRequestsPerIp);
}
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService);
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
@Bean
public PersistentTokenRepository persistentTokenRepository() {
return new JPATokenRepositoryImpl();
}
}

View File

@@ -0,0 +1,117 @@
package stirling.software.SPDF.config.security;
import java.io.IOException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Lazy;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import stirling.software.SPDF.model.ApiKeyAuthenticationToken;
@Component
public class UserAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
@Lazy
private UserService userService;
@Autowired
@Qualifier("loginEnabled")
public boolean loginEnabledValue;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
if (!loginEnabledValue) {
// If login is not enabled, just pass all requests without authentication
filterChain.doFilter(request, response);
return;
}
String requestURI = request.getRequestURI();
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// Check for API key in the request headers if no authentication exists
if (authentication == null || !authentication.isAuthenticated()) {
String apiKey = request.getHeader("X-API-Key");
if (apiKey != null && !apiKey.trim().isEmpty()) {
try {
// Use API key to authenticate. This requires you to have an authentication provider for API keys.
UserDetails userDetails = userService.loadUserByApiKey(apiKey);
if(userDetails == null)
{
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().write("Invalid API Key.");
return;
}
authentication = new ApiKeyAuthenticationToken(userDetails, apiKey, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
} catch (AuthenticationException e) {
// If API key authentication fails, deny the request
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().write("Invalid API Key.");
return;
}
}
}
// If we still don't have any authentication, deny the request
if (authentication == null || !authentication.isAuthenticated()) {
String method = request.getMethod();
String contextPath = request.getContextPath();
if ("GET".equalsIgnoreCase(method) && ! (contextPath + "/login").equals(requestURI)) {
response.sendRedirect(contextPath + "/login"); // redirect to the login page
return;
} else {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().write("Authentication required. Please provide a X-API-KEY in request header.\nThis is found in Settings -> Account Settings -> API Key\nAlternativly you can disable authentication if this is unexpected");
return;
}
}
filterChain.doFilter(request, response);
}
@Override
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
String uri = request.getRequestURI();
String contextPath = request.getContextPath();
String[] permitAllPatterns = {
contextPath + "/login",
contextPath + "/register",
contextPath + "/error",
contextPath + "/images/",
contextPath + "/public/",
contextPath + "/css/",
contextPath + "/js/",
contextPath + "/pdfjs/",
contextPath + "/site.webmanifest"
};
for (String pattern : permitAllPatterns) {
if (uri.startsWith(pattern) || uri.endsWith(".svg")) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,125 @@
package stirling.software.SPDF.config.security;
import java.io.IOException;
import java.time.Duration;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import io.github.bucket4j.Bandwidth;
import io.github.bucket4j.Bucket;
import io.github.bucket4j.ConsumptionProbe;
import io.github.bucket4j.Refill;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import stirling.software.SPDF.model.Role;
@Component
public class UserBasedRateLimitingFilter extends OncePerRequestFilter {
private final Map<String, Bucket> apiBuckets = new ConcurrentHashMap<>();
private final Map<String, Bucket> webBuckets = new ConcurrentHashMap<>();
@Autowired
private UserDetailsService userDetailsService;
@Autowired
@Qualifier("rateLimit")
public boolean rateLimit;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
if (!rateLimit) {
// If rateLimit is not enabled, just pass all requests without rate limiting
filterChain.doFilter(request, response);
return;
}
String method = request.getMethod();
if (!"POST".equalsIgnoreCase(method)) {
// If the request is not a POST, just pass it through without rate limiting
filterChain.doFilter(request, response);
return;
}
String identifier = null;
// Check for API key in the request headers
String apiKey = request.getHeader("X-API-Key");
if (apiKey != null && !apiKey.trim().isEmpty()) {
identifier = "API_KEY_" + apiKey; // Prefix to distinguish between API keys and usernames
} else {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.isAuthenticated()) {
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
identifier = userDetails.getUsername();
}
}
// If neither API key nor an authenticated user is present, use IP address
if (identifier == null) {
identifier = request.getRemoteAddr();
}
Role userRole = getRoleFromAuthentication(SecurityContextHolder.getContext().getAuthentication());
if (request.getHeader("X-API-Key") != null) {
// It's an API call
processRequest(userRole.getApiCallsPerDay(), identifier, apiBuckets, request, response, filterChain);
} else {
// It's a Web UI call
processRequest(userRole.getWebCallsPerDay(), identifier, webBuckets, request, response, filterChain);
}
}
private Role getRoleFromAuthentication(Authentication authentication) {
if (authentication != null && authentication.isAuthenticated()) {
for (GrantedAuthority authority : authentication.getAuthorities()) {
try {
return Role.fromString(authority.getAuthority());
} catch (IllegalArgumentException ex) {
// Ignore and continue to next authority.
}
}
}
throw new IllegalStateException("User does not have a valid role.");
}
private void processRequest(int limitPerDay, String identifier, Map<String, Bucket> buckets,
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws IOException, ServletException {
Bucket userBucket = buckets.computeIfAbsent(identifier, k -> createUserBucket(limitPerDay));
ConsumptionProbe probe = userBucket.tryConsumeAndReturnRemaining(1);
if (probe.isConsumed()) {
response.setHeader("X-Rate-Limit-Remaining", Long.toString(probe.getRemainingTokens()));
filterChain.doFilter(request, response);
} else {
long waitForRefill = probe.getNanosToWaitForRefill() / 1_000_000_000;
response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
response.setHeader("X-Rate-Limit-Retry-After-Seconds", String.valueOf(waitForRefill));
response.getWriter().write("Rate limit exceeded for POST requests.");
}
}
private Bucket createUserBucket(int limitPerDay) {
Bandwidth limit = Bandwidth.classic(limitPerDay, Refill.intervally(limitPerDay, Duration.ofDays(1)));
return Bucket.builder().addLimit(limit).build();
}
}

View File

@@ -0,0 +1,198 @@
package stirling.software.SPDF.config.security;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import stirling.software.SPDF.controller.api.pipeline.UserServiceInterface;
import stirling.software.SPDF.model.Authority;
import stirling.software.SPDF.model.Role;
import stirling.software.SPDF.model.User;
import stirling.software.SPDF.repository.UserRepository;
@Service
public class UserService implements UserServiceInterface{
@Autowired
private UserRepository userRepository;
@Autowired
private PasswordEncoder passwordEncoder;
public Authentication getAuthentication(String apiKey) {
User user = getUserByApiKey(apiKey);
if (user == null) {
throw new UsernameNotFoundException("API key is not valid");
}
// Convert the user into an Authentication object
return new UsernamePasswordAuthenticationToken(
user, // principal (typically the user)
null, // credentials (we don't expose the password or API key here)
getAuthorities(user) // user's authorities (roles/permissions)
);
}
private Collection<? extends GrantedAuthority> getAuthorities(User user) {
// Convert each Authority object into a SimpleGrantedAuthority object.
return user.getAuthorities().stream()
.map((Authority authority) -> new SimpleGrantedAuthority(authority.getAuthority()))
.collect(Collectors.toList());
}
private String generateApiKey() {
String apiKey;
do {
apiKey = UUID.randomUUID().toString();
} while (userRepository.findByApiKey(apiKey) != null); // Ensure uniqueness
return apiKey;
}
public User addApiKeyToUser(String username) {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
user.setApiKey(generateApiKey());
return userRepository.save(user);
}
public User refreshApiKeyForUser(String username) {
return addApiKeyToUser(username); // reuse the add API key method for refreshing
}
public String getApiKeyForUser(String username) {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
return user.getApiKey();
}
public boolean isValidApiKey(String apiKey) {
return userRepository.findByApiKey(apiKey) != null;
}
public User getUserByApiKey(String apiKey) {
return userRepository.findByApiKey(apiKey);
}
public UserDetails loadUserByApiKey(String apiKey) {
User userOptional = userRepository.findByApiKey(apiKey);
if (userOptional != null) {
User user = userOptional;
// Convert your User entity to a UserDetails object with authorities
return new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPassword(), // you might not need this for API key auth
getAuthorities(user)
);
}
return null; // or throw an exception
}
public boolean validateApiKeyForUser(String username, String apiKey) {
Optional<User> userOpt = userRepository.findByUsername(username);
return userOpt.isPresent() && userOpt.get().getApiKey().equals(apiKey);
}
public void saveUser(String username, String password) {
User user = new User();
user.setUsername(username);
user.setPassword(passwordEncoder.encode(password));
user.setEnabled(true);
userRepository.save(user);
}
public void saveUser(String username, String password, String role, boolean firstLogin) {
User user = new User();
user.setUsername(username);
user.setPassword(passwordEncoder.encode(password));
user.addAuthority(new Authority(role, user));
user.setEnabled(true);
user.setFirstLogin(firstLogin);
userRepository.save(user);
}
public void saveUser(String username, String password, String role) {
User user = new User();
user.setUsername(username);
user.setPassword(passwordEncoder.encode(password));
user.addAuthority(new Authority(role, user));
user.setEnabled(true);
user.setFirstLogin(false);
userRepository.save(user);
}
public void deleteUser(String username) {
Optional<User> userOpt = userRepository.findByUsername(username);
if (userOpt.isPresent()) {
for (Authority authority : userOpt.get().getAuthorities()) {
if (authority.getAuthority().equals(Role.INTERNAL_API_USER.getRoleId())) {
return;
}
}
userRepository.delete(userOpt.get());
}
}
public boolean usernameExists(String username) {
return userRepository.findByUsername(username).isPresent();
}
public boolean hasUsers() {
return userRepository.count() > 0;
}
public void updateUserSettings(String username, Map<String, String> updates) {
Optional<User> userOpt = userRepository.findByUsername(username);
if (userOpt.isPresent()) {
User user = userOpt.get();
Map<String, String> settingsMap = user.getSettings();
if(settingsMap == null) {
settingsMap = new HashMap<String,String>();
}
settingsMap.clear();
settingsMap.putAll(updates);
user.setSettings(settingsMap);
userRepository.save(user);
}
}
public Optional<User> findByUsername(String username) {
return userRepository.findByUsername(username);
}
public void changeUsername(User user, String newUsername) {
user.setUsername(newUsername);
userRepository.save(user);
}
public void changePassword(User user, String newPassword) {
user.setPassword(passwordEncoder.encode(newPassword));
userRepository.save(user);
}
public void changeFirstUse(User user, boolean firstUse) {
user.setFirstLogin(firstUse);
userRepository.save(user);
}
public boolean isPasswordCorrect(User user, String currentPassword) {
return passwordEncoder.matches(currentPassword, user.getPassword());
}
}

View File

@@ -1,132 +1,86 @@
package stirling.software.SPDF.controller.api;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
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.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.utils.GeneralUtils;
import stirling.software.SPDF.utils.WebResponseUtils;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.pdfbox.multipdf.LayerUtility;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import com.itextpdf.kernel.geom.PageSize;
import com.itextpdf.kernel.geom.Rectangle;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfPage;
import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.kernel.pdf.canvas.PdfCanvas;
import com.itextpdf.kernel.pdf.canvas.parser.EventType;
import com.itextpdf.kernel.pdf.canvas.parser.PdfCanvasProcessor;
import com.itextpdf.kernel.pdf.canvas.parser.data.IEventData;
import com.itextpdf.kernel.pdf.canvas.parser.data.TextRenderInfo;
import com.itextpdf.kernel.pdf.canvas.parser.listener.IEventListener;
import com.itextpdf.kernel.pdf.xobject.PdfFormXObject;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.general.CropPdfForm;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@RequestMapping("/api/v1/general")
@Tag(name = "General", description = "General APIs")
public class CropController {
private static final Logger logger = LoggerFactory.getLogger(CropController.class);
private static final Logger logger = LoggerFactory.getLogger(CropController.class);
@PostMapping(value = "/crop", consumes = "multipart/form-data")
@Operation(summary = "Crops a PDF document", description = "This operation takes an input PDF file and crops it according to the given coordinates. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> cropPdf(
@Parameter(description = "The input PDF file", required = true) @RequestParam("fileInput") MultipartFile file,
@Parameter(description = "The x-coordinate of the top-left corner of the crop area", required = true, schema = @Schema(type = "number")) @RequestParam("x") float x,
@Parameter(description = "The y-coordinate of the top-left corner of the crop area", required = true, schema = @Schema(type = "number")) @RequestParam("y") float y,
@Parameter(description = "The width of the crop area", required = true, schema = @Schema(type = "number")) @RequestParam("width") float width,
@Parameter(description = "The height of the crop area", required = true, schema = @Schema(type = "number")) @RequestParam("height") float height) throws IOException {
byte[] bytes = file.getBytes();
System.out.println("x=" + x + ", " + "y=" + y + ", " + "width=" + width + ", " +"height=" + height );
PdfReader reader = new PdfReader(new ByteArrayInputStream(bytes));
PdfDocument pdfDoc = new PdfDocument(reader);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
PdfWriter writer = new PdfWriter(baos);
PdfDocument outputPdf = new PdfDocument(writer);
int totalPages = pdfDoc.getNumberOfPages();
for (int i = 1; i <= totalPages; i++) {
PdfPage page = outputPdf.addNewPage(new PageSize(width, height));
PdfCanvas pdfCanvas = new PdfCanvas(page);
public ResponseEntity<byte[]> cropPdf(@ModelAttribute CropPdfForm form)
throws IOException {
PdfFormXObject formXObject = pdfDoc.getPage(i).copyAsFormXObject(outputPdf);
// Save the graphics state, apply the transformations, add the object, and then
// restore the graphics state
pdfCanvas.saveState();
pdfCanvas.rectangle(x, y, width, height);
pdfCanvas.clip();
pdfCanvas.addXObject(formXObject, -x, -y);
pdfCanvas.restoreState();
}
outputPdf.close();
byte[] pdfContent = baos.toByteArray();
pdfDoc.close();
return WebResponseUtils.bytesToWebResponse(pdfContent,
file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_cropped.pdf");
PDDocument sourceDocument = PDDocument.load(new ByteArrayInputStream(form.getFileInput().getBytes()));
PDDocument newDocument = new PDDocument();
int totalPages = sourceDocument.getNumberOfPages();
LayerUtility layerUtility = new LayerUtility(newDocument);
for (int i = 0; i < totalPages; i++) {
PDPage sourcePage = sourceDocument.getPage(i);
// Create a new page with the size of the source page
PDPage newPage = new PDPage(sourcePage.getMediaBox());
newDocument.addPage(newPage);
PDPageContentStream contentStream = new PDPageContentStream(newDocument, newPage);
// Import the source page as a form XObject
PDFormXObject formXObject = layerUtility.importPageAsForm(sourceDocument, i);
contentStream.saveGraphicsState();
// Define the crop area
contentStream.addRect(form.getX(), form.getY(), form.getWidth(), form.getHeight());
contentStream.clip();
// Draw the entire formXObject
contentStream.drawForm(formXObject);
contentStream.restoreGraphicsState();
contentStream.close();
// Now, set the new page's media box to the cropped size
newPage.setMediaBox(new PDRectangle(form.getX(), form.getY(), form.getWidth(), form.getHeight()));
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
newDocument.save(baos);
newDocument.close();
sourceDocument.close();
byte[] pdfContent = baos.toByteArray();
return WebResponseUtils.bytesToWebResponse(pdfContent, form.getFileInput().getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_cropped.pdf");
}
}

View File

@@ -1,80 +1,114 @@
package stirling.software.SPDF.controller.api;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.apache.pdfbox.io.MemoryUsageSetting;
import org.apache.pdfbox.multipdf.PDFMergerUtility;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageTree;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.RequestPart;
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.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.general.MergePdfsRequest;
import stirling.software.SPDF.utils.WebResponseUtils;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
@RestController
@RequestMapping("/api/v1/general")
@Tag(name = "General", description = "General APIs")
public class MergeController {
private static final Logger logger = LoggerFactory.getLogger(MergeController.class);
private PDDocument mergeDocuments(List<PDDocument> documents) throws IOException {
// Create a new empty document
PDDocument mergedDoc = new PDDocument();
// Iterate over the list of documents and add their pages to the merged document
private PDDocument mergeDocuments(List<PDDocument> documents) throws IOException {
PDDocument mergedDoc = new PDDocument();
for (PDDocument doc : documents) {
// Get all pages from the current document
PDPageTree pages = doc.getPages();
// Iterate over the pages and add them to the merged document
for (PDPage page : pages) {
for (PDPage page : doc.getPages()) {
mergedDoc.addPage(page);
}
}
// Return the merged document
return mergedDoc;
}
@PostMapping(consumes = "multipart/form-data", value = "/merge-pdfs")
@Operation(
summary = "Merge multiple PDF files into one",
description = "This endpoint merges multiple PDF files into a single PDF file. The merged file will contain all pages from the input files in the order they were provided. Input:PDF Output:PDF Type:MISO"
)
public ResponseEntity<byte[]> mergePdfs(
@RequestPart(required = true, value = "fileInput")
@Parameter(description = "The input PDF files to be merged into a single file", required = true)
MultipartFile[] files) throws IOException {
// Read the input PDF files into PDDocument objects
List<PDDocument> documents = new ArrayList<>();
// Loop through the files array and read each file into a PDDocument
for (MultipartFile file : files) {
documents.add(PDDocument.load(file.getInputStream()));
private Comparator<MultipartFile> getSortComparator(String sortType) {
switch (sortType) {
case "byFileName":
return Comparator.comparing(MultipartFile::getOriginalFilename);
case "byDateModified":
return (file1, file2) -> {
try {
BasicFileAttributes attr1 = Files.readAttributes(Paths.get(file1.getOriginalFilename()), BasicFileAttributes.class);
BasicFileAttributes attr2 = Files.readAttributes(Paths.get(file2.getOriginalFilename()), BasicFileAttributes.class);
return attr1.lastModifiedTime().compareTo(attr2.lastModifiedTime());
} catch (IOException e) {
return 0; // If there's an error, treat them as equal
}
};
case "byDateCreated":
return (file1, file2) -> {
try {
BasicFileAttributes attr1 = Files.readAttributes(Paths.get(file1.getOriginalFilename()), BasicFileAttributes.class);
BasicFileAttributes attr2 = Files.readAttributes(Paths.get(file2.getOriginalFilename()), BasicFileAttributes.class);
return attr1.creationTime().compareTo(attr2.creationTime());
} catch (IOException e) {
return 0; // If there's an error, treat them as equal
}
};
case "byPDFTitle":
return (file1, file2) -> {
try (PDDocument doc1 = PDDocument.load(file1.getInputStream());
PDDocument doc2 = PDDocument.load(file2.getInputStream())) {
String title1 = doc1.getDocumentInformation().getTitle();
String title2 = doc2.getDocumentInformation().getTitle();
return title1.compareTo(title2);
} catch (IOException e) {
return 0;
}
};
case "orderProvided":
default:
return (file1, file2) -> 0; // Default is the order provided
}
PDDocument mergedDoc = mergeDocuments(documents);
// Return the merged PDF as a response
ResponseEntity<byte[]> response = WebResponseUtils.pdfDocToWebResponse(mergedDoc, files[0].getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_merged.pdf");
for (PDDocument doc : documents) {
// Close the document after processing
doc.close();
}
return response;
}
}
@PostMapping(consumes = "multipart/form-data", value = "/merge-pdfs")
@Operation(summary = "Merge multiple PDF files into one",
description = "This endpoint merges multiple PDF files into a single PDF file. The merged file will contain all pages from the input files in the order they were provided. Input:PDF Output:PDF Type:MISO")
public ResponseEntity<byte[]> mergePdfs(@ModelAttribute MergePdfsRequest form) throws IOException {
try {
MultipartFile[] files = form.getFileInput();
Arrays.sort(files, getSortComparator(form.getSortType()));
PDFMergerUtility mergedDoc = new PDFMergerUtility();
ByteArrayOutputStream docOutputstream = new ByteArrayOutputStream();
for (MultipartFile file : files) {
mergedDoc.addSource(new ByteArrayInputStream(file.getBytes()));
}
mergedDoc.setDestinationFileName(files[0].getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_merged.pdf");
mergedDoc.setDestinationStream(docOutputstream);
mergedDoc.mergeDocuments(MemoryUsageSetting.setupMainMemoryOnly());
return WebResponseUtils.bytesToWebResponse(docOutputstream.toByteArray(), mergedDoc.getDestinationFileName());
} catch (Exception ex) {
logger.error("Error in merge pdf process", ex);
throw ex;
}
}
}

View File

@@ -1,101 +1,124 @@
package stirling.software.SPDF.controller.api;
import java.io.ByteArrayInputStream;
import java.awt.Color;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import org.apache.pdfbox.multipdf.LayerUtility;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
import org.apache.pdfbox.util.Matrix;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.RequestParam;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import com.itextpdf.kernel.geom.PageSize;
import com.itextpdf.kernel.geom.Rectangle;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfPage;
import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.kernel.pdf.canvas.PdfCanvas;
import com.itextpdf.kernel.pdf.xobject.PdfFormXObject;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.general.MergeMultiplePagesRequest;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@RequestMapping("/api/v1/general")
@Tag(name = "General", description = "General APIs")
public class MultiPageLayoutController {
private static final Logger logger = LoggerFactory.getLogger(MultiPageLayoutController.class);
@PostMapping(value = "/multi-page-layout", consumes = "multipart/form-data")
@Operation(summary = "Merge multiple pages of a PDF document into a single page", description = "This operation takes an input PDF file and the number of pages to merge into a single sheet in the output PDF file. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> mergeMultiplePagesIntoOne(
@Parameter(description = "The input PDF file", required = true) @RequestParam("fileInput") MultipartFile file,
@Parameter(description = "The number of pages to fit onto a single sheet in the output PDF. Acceptable values are 2, 3, 4, 9, 16.", required = true, schema = @Schema(type = "integer", allowableValues = {
"2", "3", "4", "9", "16" })) @RequestParam("pagesPerSheet") int pagesPerSheet)
throws IOException {
@Operation(
summary = "Merge multiple pages of a PDF document into a single page",
description = "This operation takes an input PDF file and the number of pages to merge into a single sheet in the output PDF file. Input:PDF Output:PDF Type:SISO"
)
public ResponseEntity<byte[]> mergeMultiplePagesIntoOne(@ModelAttribute MergeMultiplePagesRequest request)
throws IOException {
if (pagesPerSheet != 2 && pagesPerSheet != 3
&& pagesPerSheet != (int) Math.sqrt(pagesPerSheet) * Math.sqrt(pagesPerSheet)) {
throw new IllegalArgumentException("pagesPerSheet must be 2, 3 or a perfect square");
}
int pagesPerSheet = request.getPagesPerSheet();
MultipartFile file = request.getFileInput();
boolean addBorder = request.isAddBorder();
if (pagesPerSheet != 2 && pagesPerSheet != 3 && pagesPerSheet != (int) Math.sqrt(pagesPerSheet) * Math.sqrt(pagesPerSheet)) {
throw new IllegalArgumentException("pagesPerSheet must be 2, 3 or a perfect square");
}
int cols = pagesPerSheet == 2 || pagesPerSheet == 3 ? pagesPerSheet : (int) Math.sqrt(pagesPerSheet);
int rows = pagesPerSheet == 2 || pagesPerSheet == 3 ? 1 : (int) Math.sqrt(pagesPerSheet);
int cols = pagesPerSheet == 2 || pagesPerSheet == 3 ? pagesPerSheet : (int) Math.sqrt(pagesPerSheet);
int rows = pagesPerSheet == 2 || pagesPerSheet == 3 ? 1 : (int) Math.sqrt(pagesPerSheet);
byte[] bytes = file.getBytes();
PdfReader reader = new PdfReader(new ByteArrayInputStream(bytes));
PdfDocument pdfDoc = new PdfDocument(reader);
PDDocument sourceDocument = PDDocument.load(file.getInputStream());
PDDocument newDocument = new PDDocument();
PDPage newPage = new PDPage(PDRectangle.A4);
newDocument.addPage(newPage);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
PdfWriter writer = new PdfWriter(baos);
PdfDocument outputPdf = new PdfDocument(writer);
PageSize pageSize = new PageSize(PageSize.A4.rotate());
int totalPages = sourceDocument.getNumberOfPages();
float cellWidth = newPage.getMediaBox().getWidth() / cols;
float cellHeight = newPage.getMediaBox().getHeight() / rows;
int totalPages = pdfDoc.getNumberOfPages();
float cellWidth = pageSize.getWidth() / cols;
float cellHeight = pageSize.getHeight() / rows;
PDPageContentStream contentStream = new PDPageContentStream(newDocument, newPage, PDPageContentStream.AppendMode.APPEND, true, true);
LayerUtility layerUtility = new LayerUtility(newDocument);
for (int i = 1; i <= totalPages; i += pagesPerSheet) {
PdfPage page = outputPdf.addNewPage(pageSize);
PdfCanvas pdfCanvas = new PdfCanvas(page);
float borderThickness = 1.5f; // Specify border thickness as required
contentStream.setLineWidth(borderThickness);
contentStream.setStrokingColor(Color.BLACK);
for (int i = 0; i < totalPages; i++) {
if (i != 0 && i % pagesPerSheet == 0) {
// Close the current content stream and create a new page and content stream
contentStream.close();
newPage = new PDPage(PDRectangle.A4);
newDocument.addPage(newPage);
contentStream = new PDPageContentStream(newDocument, newPage, PDPageContentStream.AppendMode.APPEND, true, true);
}
for (int row = 0; row < rows; row++) {
for (int col = 0; col < cols; col++) {
int index = i + row * cols + col;
if (index <= totalPages) {
// Get the page and calculate scaling factors
Rectangle rect = pdfDoc.getPage(index).getPageSize();
float scaleWidth = cellWidth / rect.getWidth();
float scaleHeight = cellHeight / rect.getHeight();
float scale = Math.min(scaleWidth, scaleHeight);
PDPage sourcePage = sourceDocument.getPage(i);
PDRectangle rect = sourcePage.getMediaBox();
float scaleWidth = cellWidth / rect.getWidth();
float scaleHeight = cellHeight / rect.getHeight();
float scale = Math.min(scaleWidth, scaleHeight);
PdfFormXObject formXObject = pdfDoc.getPage(index).copyAsFormXObject(outputPdf);
float x = col * cellWidth + (cellWidth - rect.getWidth() * scale) / 2;
float y = (rows - 1 - row) * cellHeight + (cellHeight - rect.getHeight() * scale) / 2;
int adjustedPageIndex = i % pagesPerSheet; // This will reset the index for every new page
int rowIndex = adjustedPageIndex / cols;
int colIndex = adjustedPageIndex % cols;
// Save the graphics state, apply the transformations, add the object, and then
// restore the graphics state
pdfCanvas.saveState();
pdfCanvas.concatMatrix(scale, 0, 0, scale, x, y);
pdfCanvas.addXObject(formXObject, 0, 0);
pdfCanvas.restoreState();
}
}
}
}
float x = colIndex * cellWidth + (cellWidth - rect.getWidth() * scale) / 2;
float y = newPage.getMediaBox().getHeight() - ((rowIndex + 1) * cellHeight - (cellHeight - rect.getHeight() * scale) / 2);
outputPdf.close();
byte[] pdfContent = baos.toByteArray();
pdfDoc.close();
return WebResponseUtils.bytesToWebResponse(pdfContent, file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_layoutChanged.pdf");
contentStream.saveGraphicsState();
contentStream.transform(Matrix.getTranslateInstance(x, y));
contentStream.transform(Matrix.getScaleInstance(scale, scale));
PDFormXObject formXObject = layerUtility.importPageAsForm(sourceDocument, i);
contentStream.drawForm(formXObject);
contentStream.restoreGraphicsState();
if(addBorder) {
// Draw border around each page
float borderX = colIndex * cellWidth;
float borderY = newPage.getMediaBox().getHeight() - (rowIndex + 1) * cellHeight;
contentStream.addRect(borderX, borderY, cellWidth, cellHeight);
contentStream.stroke();
}
}
contentStream.close(); // Close the final content stream
sourceDocument.close();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
newDocument.save(baos);
newDocument.close();
byte[] result = baos.toByteArray();
return WebResponseUtils.bytesToWebResponse(result, file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_layoutChanged.pdf");
}
}

View File

@@ -0,0 +1,173 @@
package stirling.software.SPDF.controller.api;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.ArrayList;
import org.apache.pdfbox.multipdf.Overlay;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.general.OverlayPdfsRequest;
import stirling.software.SPDF.utils.GeneralUtils;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@RequestMapping("/api/v1/general")
@Tag(name = "General", description = "General APIs")
public class PdfOverlayController {
@PostMapping(value = "/overlay-pdfs", consumes = "multipart/form-data")
@Operation(summary = "Overlay PDF files in various modes", description = "Overlay PDF files onto a base PDF with different modes: Sequential, Interleaved, or Fixed Repeat. Input:PDF Output:PDF Type:MIMO")
public ResponseEntity<byte[]> overlayPdfs(@ModelAttribute OverlayPdfsRequest request) throws IOException {
MultipartFile baseFile = request.getFileInput();
int overlayPos = request.getOverlayPosition();
MultipartFile[] overlayFiles = request.getOverlayFiles();
File[] overlayPdfFiles = new File[overlayFiles.length];
List<File> tempFiles = new ArrayList<>(); // List to keep track of temporary files
try {
for (int i = 0; i < overlayFiles.length; i++) {
overlayPdfFiles[i] = GeneralUtils.multipartToFile(overlayFiles[i]);
}
String mode = request.getOverlayMode(); // "SequentialOverlay", "InterleavedOverlay", "FixedRepeatOverlay"
int[] counts = request.getCounts(); // Used for FixedRepeatOverlay mode
try (PDDocument basePdf = PDDocument.load(baseFile.getInputStream());
Overlay overlay = new Overlay()) {
Map<Integer, String> overlayGuide = prepareOverlayGuide(basePdf.getNumberOfPages(), overlayPdfFiles, mode, counts, tempFiles);
overlay.setInputPDF(basePdf);
if (overlayPos == 0) {
overlay.setOverlayPosition(Overlay.Position.FOREGROUND);
} else {
overlay.setOverlayPosition(Overlay.Position.BACKGROUND);
}
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
overlay.overlay(overlayGuide).save(outputStream);
byte[] data = outputStream.toByteArray();
String outputFilename = baseFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_overlayed.pdf"; // Remove file extension and append .pdf
return WebResponseUtils.bytesToWebResponse(data, outputFilename, MediaType.APPLICATION_PDF);
}
} finally {
for (File overlayPdfFile : overlayPdfFiles) {
if (overlayPdfFile != null) {
overlayPdfFile.delete();
}
}
for (File tempFile : tempFiles) { // Delete temporary files
if (tempFile != null) {
tempFile.delete();
}
}
}
}
private Map<Integer, String> prepareOverlayGuide(int basePageCount, File[] overlayFiles, String mode, int[] counts, List<File> tempFiles) throws IOException {
Map<Integer, String> overlayGuide = new HashMap<>();
switch (mode) {
case "SequentialOverlay":
sequentialOverlay(overlayGuide, overlayFiles, basePageCount, tempFiles);
break;
case "InterleavedOverlay":
interleavedOverlay(overlayGuide, overlayFiles, basePageCount);
break;
case "FixedRepeatOverlay":
fixedRepeatOverlay(overlayGuide, overlayFiles, counts, basePageCount);
break;
default:
throw new IllegalArgumentException("Invalid overlay mode");
}
return overlayGuide;
}
private void sequentialOverlay(Map<Integer, String> overlayGuide, File[] overlayFiles, int basePageCount, List<File> tempFiles) throws IOException {
int overlayFileIndex = 0;
int pageCountInCurrentOverlay = 0;
for (int basePageIndex = 1; basePageIndex <= basePageCount; basePageIndex++) {
if (pageCountInCurrentOverlay == 0 || pageCountInCurrentOverlay >= getNumberOfPages(overlayFiles[overlayFileIndex])) {
pageCountInCurrentOverlay = 0;
overlayFileIndex = (overlayFileIndex + 1) % overlayFiles.length;
}
try (PDDocument overlayPdf = PDDocument.load(overlayFiles[overlayFileIndex])) {
PDDocument singlePageDocument = new PDDocument();
singlePageDocument.addPage(overlayPdf.getPage(pageCountInCurrentOverlay));
File tempFile = File.createTempFile("overlay-page-", ".pdf");
singlePageDocument.save(tempFile);
singlePageDocument.close();
overlayGuide.put(basePageIndex, tempFile.getAbsolutePath());
tempFiles.add(tempFile); // Keep track of the temporary file for cleanup
}
pageCountInCurrentOverlay++;
}
}
private int getNumberOfPages(File file) throws IOException {
try (PDDocument doc = PDDocument.load(file)) {
return doc.getNumberOfPages();
}
}
private void interleavedOverlay(Map<Integer, String> overlayGuide, File[] overlayFiles, int basePageCount) throws IOException {
for (int basePageIndex = 1; basePageIndex <= basePageCount; basePageIndex++) {
File overlayFile = overlayFiles[(basePageIndex - 1) % overlayFiles.length];
// Load the overlay document to check its page count
try (PDDocument overlayPdf = PDDocument.load(overlayFile)) {
int overlayPageCount = overlayPdf.getNumberOfPages();
if ((basePageIndex - 1) % overlayPageCount < overlayPageCount) {
overlayGuide.put(basePageIndex, overlayFile.getAbsolutePath());
}
}
}
}
private void fixedRepeatOverlay(Map<Integer, String> overlayGuide, File[] overlayFiles, int[] counts, int basePageCount) throws IOException {
if (overlayFiles.length != counts.length) {
throw new IllegalArgumentException("Counts array length must match the number of overlay files");
}
int currentPage = 1;
for (int i = 0; i < overlayFiles.length; i++) {
File overlayFile = overlayFiles[i];
int repeatCount = counts[i];
// Load the overlay document to check its page count
try (PDDocument overlayPdf = PDDocument.load(overlayFile)) {
int overlayPageCount = overlayPdf.getNumberOfPages();
for (int j = 0; j < repeatCount; j++) {
for (int page = 0; page < overlayPageCount; page++) {
if (currentPage > basePageCount) break;
overlayGuide.put(currentPage++, overlayFile.getAbsolutePath());
}
}
}
}
}
}
// Additional classes like OverlayPdfsRequest, WebResponseUtils, etc. are assumed to be defined elsewhere.

View File

@@ -9,20 +9,21 @@ import org.apache.pdfbox.pdmodel.PDPage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
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.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.SortTypes;
import stirling.software.SPDF.model.api.PDFWithPageNums;
import stirling.software.SPDF.model.api.general.RearrangePagesRequest;
import stirling.software.SPDF.utils.GeneralUtils;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@RequestMapping("/api/v1/general")
@Tag(name = "General", description = "General APIs")
public class RearrangePagesPDFController {
@@ -30,11 +31,12 @@ public class RearrangePagesPDFController {
@PostMapping(consumes = "multipart/form-data", value = "/remove-pages")
@Operation(summary = "Remove pages from a PDF file", description = "This endpoint removes specified pages from a given PDF file. Users can provide a comma-separated list of page numbers or ranges to delete. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> deletePages(
@RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file from which pages will be removed") MultipartFile pdfFile,
@RequestParam("pagesToDelete") @Parameter(description = "Comma-separated list of pages or page ranges to delete, e.g., '1,3,5-8'") String pagesToDelete)
public ResponseEntity<byte[]> deletePages(@ModelAttribute PDFWithPageNums request )
throws IOException {
MultipartFile pdfFile = request.getFileInput();
String pagesToDelete = request.getPageNumbers();
PDDocument document = PDDocument.load(pdfFile.getBytes());
// Split the page order string into an array of page numbers or range of numbers
@@ -51,9 +53,7 @@ public class RearrangePagesPDFController {
}
private enum CustomMode {
REVERSE_ORDER, DUPLEX_SORT, BOOKLET_SORT, ODD_EVEN_SPLIT, REMOVE_FIRST, REMOVE_LAST, REMOVE_FIRST_AND_LAST,
}
private List<Integer> removeFirst(int totalPages) {
if (totalPages <= 1)
@@ -114,6 +114,18 @@ public class RearrangePagesPDFController {
return newPageOrder;
}
private List<Integer> sideStitchBooklet(int totalPages) {
List<Integer> newPageOrder = new ArrayList<>();
for (int i = 0; i < (totalPages + 3) / 4; i++) {
int begin = i * 4;
newPageOrder.add(Math.min(begin + 3, totalPages - 1));
newPageOrder.add(Math.min(begin, totalPages - 1));
newPageOrder.add(Math.min(begin + 1, totalPages - 1));
newPageOrder.add(Math.min(begin + 2, totalPages - 1));
}
return newPageOrder;
}
private List<Integer> oddEvenSplit(int totalPages) {
List<Integer> newPageOrder = new ArrayList<>();
for (int i = 1; i <= totalPages; i += 2) {
@@ -125,9 +137,9 @@ public class RearrangePagesPDFController {
return newPageOrder;
}
private List<Integer> processCustomMode(String customMode, int totalPages) {
private List<Integer> processSortTypes(String sortTypes, int totalPages) {
try {
CustomMode mode = CustomMode.valueOf(customMode.toUpperCase());
SortTypes mode = SortTypes.valueOf(sortTypes.toUpperCase());
switch (mode) {
case REVERSE_ORDER:
return reverseOrder(totalPages);
@@ -135,6 +147,8 @@ public class RearrangePagesPDFController {
return duplexSort(totalPages);
case BOOKLET_SORT:
return bookletSort(totalPages);
case SIDE_STITCH_BOOKLET_SORT:
return sideStitchBooklet(totalPages);
case ODD_EVEN_SPLIT:
return oddEvenSplit(totalPages);
case REMOVE_FIRST:
@@ -154,16 +168,10 @@ public class RearrangePagesPDFController {
@PostMapping(consumes = "multipart/form-data", value = "/rearrange-pages")
@Operation(summary = "Rearrange pages in a PDF file", description = "This endpoint rearranges pages in a given PDF file based on the specified page order or custom mode. Users can provide a page order as a comma-separated list of page numbers or page ranges, or a custom mode. Input:PDF Output:PDF")
public ResponseEntity<byte[]> rearrangePages(
@RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file to rearrange pages") MultipartFile pdfFile,
@RequestParam(required = false, value = "pageOrder") @Parameter(description = "The new page order as a comma-separated list of page numbers, page ranges (e.g., '1,3,5-7'), 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')") String pageOrder,
@RequestParam(required = false, value = "customMode") @Parameter(schema = @Schema(implementation = CustomMode.class, description = "The custom mode for page rearrangement. "
+ "Valid values are:\n" + "REVERSE_ORDER: Reverses the order of all pages.\n"
+ "DUPLEX_SORT: Sorts pages as if all fronts were scanned then all backs in reverse (1, n, 2, n-1, ...). "
+ "BOOKLET_SORT: Arranges pages for booklet printing (last, first, second, second last, ...).\n"
+ "ODD_EVEN_SPLIT: Splits and arranges pages into odd and even numbered pages.\n"
+ "REMOVE_FIRST: Removes the first page.\n" + "REMOVE_LAST: Removes the last page.\n"
+ "REMOVE_FIRST_AND_LAST: Removes both the first and the last pages.\n")) String customMode) {
public ResponseEntity<byte[]> rearrangePages(@ModelAttribute RearrangePagesRequest request) throws IOException {
MultipartFile pdfFile = request.getFileInput();
String pageOrder = request.getPageNumbers();
String sortType = request.getCustomMode();
try {
// Load the input PDF
PDDocument document = PDDocument.load(pdfFile.getInputStream());
@@ -171,15 +179,14 @@ public class RearrangePagesPDFController {
// Split the page order string into an array of page numbers or range of numbers
String[] pageOrderArr = pageOrder != null ? pageOrder.split(",") : new String[0];
int totalPages = document.getNumberOfPages();
System.out.println("pageOrder=" + pageOrder);
System.out.println("customMode length =" + customMode.length());
List<Integer> newPageOrder;
if (customMode != null && customMode.length() > 0) {
newPageOrder = processCustomMode(customMode, totalPages);
if (sortType != null && sortType.length() > 0) {
newPageOrder = processSortTypes(sortType, totalPages);
} else {
newPageOrder = GeneralUtils.parsePageList(pageOrderArr, totalPages);
}
logger.info("newPageOrder = " +newPageOrder);
logger.info("totalPages = " +totalPages);
// Create a new list to hold the pages in the new order
List<PDPage> newPages = new ArrayList<>();
for (int i = 0; i < newPageOrder.size(); i++) {

View File

@@ -8,18 +8,19 @@ import org.apache.pdfbox.pdmodel.PDPageTree;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
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.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.general.RotatePDFRequest;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@RequestMapping("/api/v1/general")
@Tag(name = "General", description = "General APIs")
public class RotationController {
@@ -31,13 +32,9 @@ public class RotationController {
description = "This endpoint rotates a given PDF file by a specified angle. The angle must be a multiple of 90. Input:PDF Output:PDF Type:SISO"
)
public ResponseEntity<byte[]> rotatePDF(
@RequestPart(required = true, value = "fileInput")
@Parameter(description = "The PDF file to be rotated", required = true)
MultipartFile pdfFile,
@RequestParam("angle")
@Parameter(description = "The angle by which to rotate the PDF file. This should be a multiple of 90.", example = "90", required = true)
Integer angle) throws IOException {
@ModelAttribute RotatePDFRequest request) throws IOException {
MultipartFile pdfFile = request.getFileInput();
Integer angle = request.getAngle();
// Load the PDF document
PDDocument document = PDDocument.load(pdfFile.getBytes());

View File

@@ -1,48 +1,32 @@
package stirling.software.SPDF.controller.api;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.pdfbox.multipdf.LayerUtility;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
import org.apache.pdfbox.util.Matrix;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import com.itextpdf.kernel.geom.PageSize;
import com.itextpdf.kernel.geom.Rectangle;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfPage;
import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.kernel.pdf.canvas.PdfCanvas;
import com.itextpdf.kernel.pdf.canvas.parser.EventType;
import com.itextpdf.kernel.pdf.canvas.parser.PdfCanvasProcessor;
import com.itextpdf.kernel.pdf.canvas.parser.data.IEventData;
import com.itextpdf.kernel.pdf.canvas.parser.data.TextRenderInfo;
import com.itextpdf.kernel.pdf.canvas.parser.listener.IEventListener;
import com.itextpdf.kernel.pdf.xobject.PdfFormXObject;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.general.ScalePagesRequest;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@RequestMapping("/api/v1/general")
@Tag(name = "General", description = "General APIs")
public class ScalePagesController {
@@ -50,194 +34,77 @@ public class ScalePagesController {
@PostMapping(value = "/scale-pages", consumes = "multipart/form-data")
@Operation(summary = "Change the size of a PDF page/document", description = "This operation takes an input PDF file and the size to scale the pages to in the output PDF file. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> scalePages(
@Parameter(description = "The input PDF file", required = true) @RequestParam("fileInput") MultipartFile file,
@Parameter(description = "The scale of pages in the output PDF. Acceptable values are A0-A10, B0-B9, LETTER, TABLOID, LEDGER, LEGAL, EXECUTIVE.", required = true, schema = @Schema(type = "string", allowableValues = {
"A0", "A1", "A2", "A3", "A4", "A5", "A6", "A7", "A8", "A9", "A10", "B0", "B1", "B2", "B3", "B4",
"B5", "B6", "B7", "B8", "B9", "LETTER", "TABLOID", "LEDGER", "LEGAL",
"EXECUTIVE" })) @RequestParam("pageSize") String targetPageSize,
@Parameter(description = "The scale of the content on the pages of the output PDF. Acceptable values are floats.", required = true, schema = @Schema(type = "integer")) @RequestParam("scaleFactor") float scaleFactor)
throws IOException {
public ResponseEntity<byte[]> scalePages(@ModelAttribute ScalePagesRequest request) throws IOException {
MultipartFile file = request.getFileInput();
String targetPDRectangle = request.getPageSize();
float scaleFactor = request.getScaleFactor();
Map<String, PageSize> sizeMap = new HashMap<>();
Map<String, PDRectangle> sizeMap = new HashMap<>();
// Add A0 - A10
sizeMap.put("A0", PageSize.A0);
sizeMap.put("A1", PageSize.A1);
sizeMap.put("A2", PageSize.A2);
sizeMap.put("A3", PageSize.A3);
sizeMap.put("A4", PageSize.A4);
sizeMap.put("A5", PageSize.A5);
sizeMap.put("A6", PageSize.A6);
sizeMap.put("A7", PageSize.A7);
sizeMap.put("A8", PageSize.A8);
sizeMap.put("A9", PageSize.A9);
sizeMap.put("A10", PageSize.A10);
// Add B0 - B9
sizeMap.put("B0", PageSize.B0);
sizeMap.put("B1", PageSize.B1);
sizeMap.put("B2", PageSize.B2);
sizeMap.put("B3", PageSize.B3);
sizeMap.put("B4", PageSize.B4);
sizeMap.put("B5", PageSize.B5);
sizeMap.put("B6", PageSize.B6);
sizeMap.put("B7", PageSize.B7);
sizeMap.put("B8", PageSize.B8);
sizeMap.put("B9", PageSize.B9);
sizeMap.put("A0", PDRectangle.A0);
sizeMap.put("A1", PDRectangle.A1);
sizeMap.put("A2", PDRectangle.A2);
sizeMap.put("A3", PDRectangle.A3);
sizeMap.put("A4", PDRectangle.A4);
sizeMap.put("A5", PDRectangle.A5);
sizeMap.put("A6", PDRectangle.A6);
// Add other sizes
sizeMap.put("LETTER", PageSize.LETTER);
sizeMap.put("TABLOID", PageSize.TABLOID);
sizeMap.put("LEDGER", PageSize.LEDGER);
sizeMap.put("LEGAL", PageSize.LEGAL);
sizeMap.put("EXECUTIVE", PageSize.EXECUTIVE);
sizeMap.put("LETTER", PDRectangle.LETTER);
sizeMap.put("LEGAL", PDRectangle.LEGAL);
if (!sizeMap.containsKey(targetPageSize)) {
if (!sizeMap.containsKey(targetPDRectangle)) {
throw new IllegalArgumentException(
"Invalid pageSize. It must be one of the following: A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10");
"Invalid PDRectangle. It must be one of the following: A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10");
}
PageSize pageSize = sizeMap.get(targetPageSize);
PDRectangle targetSize = sizeMap.get(targetPDRectangle);
byte[] bytes = file.getBytes();
PdfReader reader = new PdfReader(new ByteArrayInputStream(bytes));
PdfDocument pdfDoc = new PdfDocument(reader);
PDDocument sourceDocument = PDDocument.load(file.getBytes());
PDDocument outputDocument = new PDDocument();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
PdfWriter writer = new PdfWriter(baos);
PdfDocument outputPdf = new PdfDocument(writer);
int totalPages = sourceDocument.getNumberOfPages();
for (int i = 0; i < totalPages; i++) {
PDPage sourcePage = sourceDocument.getPage(i);
PDRectangle sourceSize = sourcePage.getMediaBox();
float scaleWidth = targetSize.getWidth() / sourceSize.getWidth();
float scaleHeight = targetSize.getHeight() / sourceSize.getHeight();
float scale = Math.min(scaleWidth, scaleHeight) * scaleFactor;
PDPage newPage = new PDPage(targetSize);
outputDocument.addPage(newPage);
PDPageContentStream contentStream = new PDPageContentStream(outputDocument, newPage, PDPageContentStream.AppendMode.APPEND, true);
float x = (targetSize.getWidth() - sourceSize.getWidth() * scale) / 2;
float y = (targetSize.getHeight() - sourceSize.getHeight() * scale) / 2;
contentStream.saveGraphicsState();
contentStream.transform(Matrix.getTranslateInstance(x, y));
contentStream.transform(Matrix.getScaleInstance(scale, scale));
LayerUtility layerUtility = new LayerUtility(outputDocument);
PDFormXObject form = layerUtility.importPageAsForm(sourceDocument, i);
contentStream.drawForm(form);
int totalPages = pdfDoc.getNumberOfPages();
contentStream.restoreGraphicsState();
contentStream.close();
}
for (int i = 1; i <= totalPages; i++) {
PdfPage page = outputPdf.addNewPage(pageSize);
PdfCanvas pdfCanvas = new PdfCanvas(page);
// Get the page and calculate scaling factors
Rectangle rect = pdfDoc.getPage(i).getPageSize();
float scaleWidth = pageSize.getWidth() / rect.getWidth();
float scaleHeight = pageSize.getHeight() / rect.getHeight();
float scale = Math.min(scaleWidth, scaleHeight) * scaleFactor;
System.out.println("Scale: " + scale);
PdfFormXObject formXObject = pdfDoc.getPage(i).copyAsFormXObject(outputPdf);
float x = (pageSize.getWidth() - rect.getWidth() * scale) / 2; // Center Page
float y = (pageSize.getHeight() - rect.getHeight() * scale) / 2;
// Save the graphics state, apply the transformations, add the object, and then
// restore the graphics state
pdfCanvas.saveState();
pdfCanvas.concatMatrix(scale, 0, 0, scale, x, y);
pdfCanvas.addXObject(formXObject, 0, 0);
pdfCanvas.restoreState();
}
outputPdf.close();
byte[] pdfContent = baos.toByteArray();
pdfDoc.close();
return WebResponseUtils.bytesToWebResponse(pdfContent,
ByteArrayOutputStream baos = new ByteArrayOutputStream();
outputDocument.save(baos);
outputDocument.close();
sourceDocument.close();
return WebResponseUtils.bytesToWebResponse(baos.toByteArray(),
file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_scaled.pdf");
}
//TODO
@Hidden
@PostMapping(value = "/auto-crop", consumes = "multipart/form-data")
public ResponseEntity<byte[]> cropPdf(@RequestParam("fileInput") MultipartFile file) throws IOException {
byte[] bytes = file.getBytes();
PdfReader reader = new PdfReader(new ByteArrayInputStream(bytes));
PdfDocument pdfDoc = new PdfDocument(reader);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
PdfWriter writer = new PdfWriter(baos);
PdfDocument outputPdf = new PdfDocument(writer);
int totalPages = pdfDoc.getNumberOfPages();
for (int i = 1; i <= totalPages; i++) {
PdfPage page = pdfDoc.getPage(i);
Rectangle originalMediaBox = page.getMediaBox();
Rectangle contentBox = determineContentBox(page);
// Make sure we don't go outside the original media box.
Rectangle intersection = originalMediaBox.getIntersection(contentBox);
page.setCropBox(intersection);
// Copy page to the new document
outputPdf.addPage(page.copyTo(outputPdf));
}
outputPdf.close();
byte[] pdfContent = baos.toByteArray();
pdfDoc.close();
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\""
+ file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_cropped.pdf\"")
.contentType(MediaType.APPLICATION_PDF).body(pdfContent);
}
private Rectangle determineContentBox(PdfPage page) {
// Extract the text from the page and find the bounding box.
TextBoundingRectangleFinder finder = new TextBoundingRectangleFinder();
PdfCanvasProcessor processor = new PdfCanvasProcessor(finder);
processor.processPageContent(page);
return finder.getBoundingBox();
}
private static class TextBoundingRectangleFinder implements IEventListener {
private List<Rectangle> allTextBoxes = new ArrayList<>();
public Rectangle getBoundingBox() {
// Sort the text boxes based on their vertical position
allTextBoxes.sort(Comparator.comparingDouble(Rectangle::getTop));
// Consider a box an outlier if its top is more than 1.5 times the IQR above the
// third quartile.
int q1Index = allTextBoxes.size() / 4;
int q3Index = 3 * allTextBoxes.size() / 4;
double iqr = allTextBoxes.get(q3Index).getTop() - allTextBoxes.get(q1Index).getTop();
double threshold = allTextBoxes.get(q3Index).getTop() + 1.5 * iqr;
// Initialize boundingBox to the first non-outlier box
int i = 0;
while (i < allTextBoxes.size() && allTextBoxes.get(i).getTop() > threshold) {
i++;
}
if (i == allTextBoxes.size()) {
// If all boxes are outliers, just return the first one
return allTextBoxes.get(0);
}
Rectangle boundingBox = allTextBoxes.get(i);
// Extend the bounding box to include all non-outlier boxes
for (; i < allTextBoxes.size(); i++) {
Rectangle textBoundingBox = allTextBoxes.get(i);
if (textBoundingBox.getTop() > threshold) {
// This box is an outlier, skip it
continue;
}
float left = Math.min(boundingBox.getLeft(), textBoundingBox.getLeft());
float bottom = Math.min(boundingBox.getBottom(), textBoundingBox.getBottom());
float right = Math.max(boundingBox.getRight(), textBoundingBox.getRight());
float top = Math.max(boundingBox.getTop(), textBoundingBox.getTop());
// Add a small padding around the bounding box
float padding = 10;
boundingBox = new Rectangle(left - padding, bottom - padding, right - left + 2 * padding,
top - bottom + 2 * padding);
}
return boundingBox;
}
@Override
public void eventOccurred(IEventData data, EventType type) {
if (type == EventType.RENDER_TEXT) {
TextRenderInfo renderInfo = (TextRenderInfo) data;
allTextBoxes.add(renderInfo.getBaseline().getBoundingRectangle());
}
}
@Override
public Set<EventType> getSupportedEvents() {
return Collections.singleton(EventType.RENDER_TEXT);
}
}
}

View File

@@ -17,19 +17,19 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
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.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.utils.GeneralUtils;
import stirling.software.SPDF.model.api.PDFWithPageNums;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@RequestMapping("/api/v1/general")
@Tag(name = "General", description = "General APIs")
public class SplitPDFController {
@@ -38,35 +38,16 @@ public class SplitPDFController {
@PostMapping(consumes = "multipart/form-data", value = "/split-pages")
@Operation(summary = "Split a PDF file into separate documents",
description = "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(
@RequestPart(required = true, value = "fileInput")
@Parameter(description = "The input PDF file to be split")
MultipartFile file,
@RequestParam("pages")
@Parameter(description = "The pages to be included in separate documents. Specify individual page numbers (e.g., '1,3,5'), ranges (e.g., '1-3,5-7'), or 'all' for every page.")
String pages) throws IOException {
// parse user input
public ResponseEntity<byte[]> splitPdf(@ModelAttribute PDFWithPageNums request) throws IOException {
MultipartFile file = request.getFileInput();
String pages = request.getPageNumbers();
// open the pdf document
InputStream inputStream = file.getInputStream();
PDDocument document = PDDocument.load(inputStream);
List<Integer> pageNumbers = new ArrayList<>();
pages = pages.replaceAll("\\s+", ""); // remove whitespaces
if (pages.toLowerCase().equals("all")) {
for (int i = 0; i < document.getNumberOfPages(); i++) {
pageNumbers.add(i);
}
} else {
String[] splitPoints = pages.split(",");
for (String splitPoint : splitPoints) {
List<Integer> orderedPages = GeneralUtils.parsePageList(new String[] {splitPoint}, document.getNumberOfPages());
pageNumbers.addAll(orderedPages);
}
// Add the last page as a split point
pageNumbers.add(document.getNumberOfPages() - 1);
}
List<Integer> pageNumbers = request.getPageNumbersList(document);
if(!pageNumbers.contains(document.getNumberOfPages() - 1))
pageNumbers.add(document.getNumberOfPages()- 1);
logger.info("Splitting PDF into pages: {}", pageNumbers.stream().map(String::valueOf).collect(Collectors.joining(",")));
// split the document
@@ -127,4 +108,4 @@ public class SplitPDFController {
}
}
}

View File

@@ -0,0 +1,135 @@
package stirling.software.SPDF.controller.api;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import org.apache.pdfbox.multipdf.LayerUtility;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
import org.apache.pdfbox.util.Matrix;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.SplitPdfBySectionsRequest;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@RequestMapping("/api/v1/general")
@Tag(name = "General", description = "General APIs")
public class SplitPdfBySectionsController {
@PostMapping(value = "/split-pdf-by-sections", consumes = "multipart/form-data")
@Operation(summary = "Split PDF pages into smaller sections", description = "Split each page of a PDF into smaller sections based on the user's choice (halves, thirds, quarters, etc.), both vertically and horizontally. Input:PDF Output:ZIP-PDF Type:SISO")
public ResponseEntity<byte[]> splitPdf(@ModelAttribute SplitPdfBySectionsRequest request) throws Exception {
List<ByteArrayOutputStream> splitDocumentsBoas = new ArrayList<>();
MultipartFile file = request.getFileInput();
PDDocument sourceDocument = PDDocument.load(file.getInputStream());
// Process the PDF based on split parameters
int horiz = request.getHorizontalDivisions() + 1;
int verti = request.getVerticalDivisions() + 1;
List<PDDocument> splitDocuments = splitPdfPages(sourceDocument, verti, horiz);
for (PDDocument doc : splitDocuments) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
doc.save(baos);
doc.close();
splitDocumentsBoas.add(baos);
}
sourceDocument.close();
Path zipFile = Files.createTempFile("split_documents", ".zip");
String filename = file.getOriginalFilename().replaceFirst("[.][^.]+$", "");
byte[] data;
try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(zipFile))) {
int pageNum = 1;
for (int i = 0; i < splitDocumentsBoas.size(); i++) {
ByteArrayOutputStream baos = splitDocumentsBoas.get(i);
int sectionNum = (i % (horiz * verti)) + 1;
String fileName = filename + "_" + pageNum + "_" + sectionNum + ".pdf";
byte[] pdf = baos.toByteArray();
ZipEntry pdfEntry = new ZipEntry(fileName);
zipOut.putNextEntry(pdfEntry);
zipOut.write(pdf);
zipOut.closeEntry();
if (sectionNum == horiz * verti) pageNum++;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
data = Files.readAllBytes(zipFile);
Files.delete(zipFile);
}
return WebResponseUtils.bytesToWebResponse(data, filename + "_split.zip", MediaType.APPLICATION_OCTET_STREAM);
}
public List<PDDocument> splitPdfPages(PDDocument document, int horizontalDivisions, int verticalDivisions) throws IOException {
List<PDDocument> splitDocuments = new ArrayList<>();
for (PDPage originalPage : document.getPages()) {
PDRectangle originalMediaBox = originalPage.getMediaBox();
float width = originalMediaBox.getWidth();
float height = originalMediaBox.getHeight();
float subPageWidth = width / horizontalDivisions;
float subPageHeight = height / verticalDivisions;
LayerUtility layerUtility = new LayerUtility(document);
for (int i = 0; i < horizontalDivisions; i++) {
for (int j = 0; j < verticalDivisions; j++) {
PDDocument subDoc = new PDDocument();
PDPage subPage = new PDPage(new PDRectangle(subPageWidth, subPageHeight));
subDoc.addPage(subPage);
PDFormXObject form = layerUtility.importPageAsForm(document, document.getPages().indexOf(originalPage));
try (PDPageContentStream contentStream = new PDPageContentStream(subDoc, subPage)) {
// Set clipping area and position
float translateX = -subPageWidth * i;
float translateY = height - subPageHeight * (verticalDivisions - j);
contentStream.saveGraphicsState();
contentStream.addRect(0, 0, subPageWidth, subPageHeight);
contentStream.clip();
contentStream.transform(new Matrix(1, 0, 0, 1, translateX, translateY));
// Draw the form
contentStream.drawForm(form);
contentStream.restoreGraphicsState();
}
splitDocuments.add(subDoc);
}
}
}
return splitDocuments;
}
}

View File

@@ -0,0 +1,153 @@
package stirling.software.SPDF.controller.api;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.general.SplitPdfBySizeOrCountRequest;
import stirling.software.SPDF.utils.GeneralUtils;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@RequestMapping("/api/v1/general")
@Tag(name = "General", description = "General APIs")
public class SplitPdfBySizeController {
@PostMapping(value = "/split-by-size-or-count", consumes = "multipart/form-data")
@Operation(summary = "Auto split PDF pages into separate documents based on size or count", description = "split PDF into multiple paged documents based on size/count, ie if 20 pages and split into 5, it does 5 documents each 4 pages\r\n"
+ " if 10MB and each page is 1MB and you enter 2MB then 5 docs each 2MB (rounded so that it accepts 1.9MB but not 2.1MB) Input:PDF Output:ZIP-PDF Type:SISO")
public ResponseEntity<byte[]> autoSplitPdf(@ModelAttribute SplitPdfBySizeOrCountRequest request) throws Exception {
List<ByteArrayOutputStream> splitDocumentsBoas = new ArrayList<ByteArrayOutputStream>();
MultipartFile file = request.getFileInput();
PDDocument sourceDocument = PDDocument.load(file.getInputStream());
//0 = size, 1 = page count, 2 = doc count
int type = request.getSplitType();
String value = request.getSplitValue();
if (type == 0) { // Split by size
long maxBytes = GeneralUtils.convertSizeToBytes(value);
long currentSize = 0;
PDDocument currentDoc = new PDDocument();
for (PDPage page : sourceDocument.getPages()) {
ByteArrayOutputStream pageOutputStream = new ByteArrayOutputStream();
PDDocument tempDoc = new PDDocument();
tempDoc.addPage(page);
tempDoc.save(pageOutputStream);
tempDoc.close();
long pageSize = pageOutputStream.size();
if (currentSize + pageSize > maxBytes) {
// Save and reset current document
splitDocumentsBoas.add(currentDocToByteArray(currentDoc));
currentDoc = new PDDocument();
currentSize = 0;
}
currentDoc.addPage(page);
currentSize += pageSize;
}
// Add the last document if it contains any pages
if (currentDoc.getPages().getCount() != 0) {
splitDocumentsBoas.add(currentDocToByteArray(currentDoc));
}
} else if (type == 1) { // Split by page count
int pageCount = Integer.parseInt(value);
int currentPageCount = 0;
PDDocument currentDoc = new PDDocument();
for (PDPage page : sourceDocument.getPages()) {
currentDoc.addPage(page);
currentPageCount++;
if (currentPageCount == pageCount) {
// Save and reset current document
splitDocumentsBoas.add(currentDocToByteArray(currentDoc));
currentDoc = new PDDocument();
currentPageCount = 0;
}
}
// Add the last document if it contains any pages
if (currentDoc.getPages().getCount() != 0) {
splitDocumentsBoas.add(currentDocToByteArray(currentDoc));
}
} else if (type == 2) { // Split by doc count
int documentCount = Integer.parseInt(value);
int totalPageCount = sourceDocument.getNumberOfPages();
int pagesPerDocument = totalPageCount / documentCount;
int extraPages = totalPageCount % documentCount;
int currentPageIndex = 0;
for (int i = 0; i < documentCount; i++) {
PDDocument currentDoc = new PDDocument();
int pagesToAdd = pagesPerDocument + (i < extraPages ? 1 : 0);
for (int j = 0; j < pagesToAdd; j++) {
currentDoc.addPage(sourceDocument.getPage(currentPageIndex++));
}
splitDocumentsBoas.add(currentDocToByteArray(currentDoc));
}
} else {
throw new IllegalArgumentException("Invalid argument for split type");
}
sourceDocument.close();
Path zipFile = Files.createTempFile("split_documents", ".zip");
String filename = file.getOriginalFilename().replaceFirst("[.][^.]+$", "");
byte[] data;
try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(zipFile))) {
for (int i = 0; i < splitDocumentsBoas.size(); i++) {
String fileName = filename + "_" + (i + 1) + ".pdf";
ByteArrayOutputStream baos = splitDocumentsBoas.get(i);
byte[] pdf = baos.toByteArray();
ZipEntry pdfEntry = new ZipEntry(fileName);
zipOut.putNextEntry(pdfEntry);
zipOut.write(pdf);
zipOut.closeEntry();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
data = Files.readAllBytes(zipFile);
Files.delete(zipFile);
}
return WebResponseUtils.bytesToWebResponse(data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM);
}
private ByteArrayOutputStream currentDocToByteArray(PDDocument document) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
document.save(baos);
document.close();
return baos;
}
}

View File

@@ -1,36 +1,29 @@
package stirling.software.SPDF.controller.api;
import java.io.IOException;
import java.awt.geom.AffineTransform;
import java.io.ByteArrayOutputStream;
import com.itextpdf.kernel.pdf.*;
import com.itextpdf.kernel.pdf.xobject.PdfFormXObject;
import com.itextpdf.kernel.geom.PageSize;
import com.itextpdf.kernel.geom.Rectangle;
import com.itextpdf.layout.Document;
import com.itextpdf.layout.element.Image;
import java.util.ArrayList;
import java.util.List;
import java.io.IOException;
import org.apache.pdfbox.multipdf.LayerUtility;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageTree;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.RequestPart;
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.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.PDFFile;
import stirling.software.SPDF.utils.WebResponseUtils;
import org.apache.pdfbox.pdmodel.*;
import org.apache.pdfbox.multipdf.PDFMergerUtility;
@RestController
@RequestMapping("/api/v1/general")
@Tag(name = "General", description = "General APIs")
public class ToSinglePageController {
@@ -42,45 +35,52 @@ public class ToSinglePageController {
summary = "Convert a multi-page PDF into a single long page PDF",
description = "This endpoint converts a multi-page PDF document into a single paged PDF document. The width of the single page will be same as the input's width, but the height will be the sum of all the pages' heights. Input:PDF Output:PDF Type:SISO"
)
public ResponseEntity<byte[]> pdfToSinglePage(
@RequestPart(required = true, value = "fileInput")
@Parameter(description = "The input multi-page PDF file to be converted into a single page", required = true)
MultipartFile file) throws IOException {
public ResponseEntity<byte[]> pdfToSinglePage(@ModelAttribute PDFFile request) throws IOException {
PdfReader reader = new PdfReader(file.getInputStream());
PdfDocument sourceDocument = new PdfDocument(reader);
float totalHeight = 0;
float width = 0;
// Load the source document
PDDocument sourceDocument = PDDocument.load(request.getFileInput().getInputStream());
for (int i = 1; i <= sourceDocument.getNumberOfPages(); i++) {
Rectangle pageSize = sourceDocument.getPage(i).getPageSize();
totalHeight += pageSize.getHeight();
if(width < pageSize.getWidth())
width = pageSize.getWidth();
}
// Calculate total height and max width
float totalHeight = 0;
float maxWidth = 0;
for (PDPage page : sourceDocument.getPages()) {
PDRectangle pageSize = page.getMediaBox();
totalHeight += pageSize.getHeight();
maxWidth = Math.max(maxWidth, pageSize.getWidth());
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
PdfWriter writer = new PdfWriter(baos);
PdfDocument newDocument = new PdfDocument(writer);
PageSize newPageSize = new PageSize(width, totalHeight);
newDocument.addNewPage(newPageSize);
// Create new document and page with calculated dimensions
PDDocument newDocument = new PDDocument();
PDPage newPage = new PDPage(new PDRectangle(maxWidth, totalHeight));
newDocument.addPage(newPage);
Document layoutDoc = new Document(newDocument);
float yOffset = totalHeight;
// Initialize the content stream of the new page
PDPageContentStream contentStream = new PDPageContentStream(newDocument, newPage);
contentStream.close();
LayerUtility layerUtility = new LayerUtility(newDocument);
float yOffset = totalHeight;
for (int i = 1; i <= sourceDocument.getNumberOfPages(); i++) {
PdfFormXObject pageCopy = sourceDocument.getPage(i).copyAsFormXObject(newDocument);
Image copiedPage = new Image(pageCopy);
copiedPage.setFixedPosition(0, yOffset - sourceDocument.getPage(i).getPageSize().getHeight());
yOffset -= sourceDocument.getPage(i).getPageSize().getHeight();
layoutDoc.add(copiedPage);
}
// For each page, copy its content to the new page at the correct offset
for (PDPage page : sourceDocument.getPages()) {
PDFormXObject form = layerUtility.importPageAsForm(sourceDocument, sourceDocument.getPages().indexOf(page));
AffineTransform af = AffineTransform.getTranslateInstance(0, yOffset - page.getMediaBox().getHeight());
layerUtility.wrapInSaveRestore(newPage);
String defaultLayerName = "Layer" + sourceDocument.getPages().indexOf(page);
layerUtility.appendFormAsLayer(newPage, form, af, defaultLayerName);
yOffset -= page.getMediaBox().getHeight();
}
layoutDoc.close();
sourceDocument.close();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
newDocument.save(baos);
newDocument.close();
sourceDocument.close();
byte[] result = baos.toByteArray();
return WebResponseUtils.bytesToWebResponse(result, file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_singlePage.pdf");
byte[] result = baos.toByteArray();
return WebResponseUtils.bytesToWebResponse(result, request.getFileInput().getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_singlePage.pdf");
}
}

View File

@@ -0,0 +1,252 @@
package stirling.software.SPDF.controller.api;
import java.security.Principal;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import org.springframework.web.servlet.view.RedirectView;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import stirling.software.SPDF.config.security.UserService;
import stirling.software.SPDF.model.Role;
import stirling.software.SPDF.model.User;
@Controller
@RequestMapping("/api/v1/user")
public class UserController {
@Autowired
private UserService userService;
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
@PostMapping("/register")
public String register(@RequestParam String username, @RequestParam String password, Model model) {
if(userService.usernameExists(username)) {
model.addAttribute("error", "Username already exists");
return "register";
}
userService.saveUser(username, password);
return "redirect:/login?registered=true";
}
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
@PostMapping("/change-username-and-password")
public RedirectView changeUsernameAndPassword(Principal principal,
@RequestParam String currentPassword,
@RequestParam String newUsername,
@RequestParam String newPassword,
HttpServletRequest request,
HttpServletResponse response,
RedirectAttributes redirectAttributes) {
if (principal == null) {
return new RedirectView("/change-creds?messageType=notAuthenticated");
}
Optional<User> userOpt = userService.findByUsername(principal.getName());
if (userOpt == null || userOpt.isEmpty()) {
return new RedirectView("/change-creds?messageType=userNotFound");
}
User user = userOpt.get();
if (!userService.isPasswordCorrect(user, currentPassword)) {
return new RedirectView("/change-creds?messageType=incorrectPassword");
}
if (!user.getUsername().equals(newUsername) && userService.usernameExists(newUsername)) {
return new RedirectView("/change-creds?messageType=usernameExists");
}
userService.changePassword(user, newPassword);
if(newUsername != null && newUsername.length() > 0 && !user.getUsername().equals(newUsername)) {
userService.changeUsername(user, newUsername);
}
userService.changeFirstUse(user, false);
// Logout using Spring's utility
new SecurityContextLogoutHandler().logout(request, response, null);
return new RedirectView("/login?messageType=credsUpdated");
}
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
@PostMapping("/change-username")
public RedirectView changeUsername(Principal principal,
@RequestParam String currentPassword,
@RequestParam String newUsername,
HttpServletRequest request,
HttpServletResponse response,
RedirectAttributes redirectAttributes) {
if (principal == null) {
return new RedirectView("/account?messageType=notAuthenticated");
}
Optional<User> userOpt = userService.findByUsername(principal.getName());
if (userOpt == null || userOpt.isEmpty()) {
return new RedirectView("/account?messageType=userNotFound");
}
User user = userOpt.get();
if (!userService.isPasswordCorrect(user, currentPassword)) {
return new RedirectView("/account?messageType=incorrectPassword");
}
if (!user.getUsername().equals(newUsername) && userService.usernameExists(newUsername)) {
return new RedirectView("/account?messageType=usernameExists");
}
if(newUsername != null && newUsername.length() > 0) {
userService.changeUsername(user, newUsername);
}
// Logout using Spring's utility
new SecurityContextLogoutHandler().logout(request, response, null);
return new RedirectView("/login?messageType=credsUpdated");
}
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
@PostMapping("/change-password")
public RedirectView changePassword(Principal principal,
@RequestParam String currentPassword,
@RequestParam String newPassword,
HttpServletRequest request,
HttpServletResponse response,
RedirectAttributes redirectAttributes) {
if (principal == null) {
return new RedirectView("/account?messageType=notAuthenticated");
}
Optional<User> userOpt = userService.findByUsername(principal.getName());
if (userOpt == null || userOpt.isEmpty()) {
return new RedirectView("/account?messageType=userNotFound");
}
User user = userOpt.get();
if (!userService.isPasswordCorrect(user, currentPassword)) {
return new RedirectView("/account?messageType=incorrectPassword");
}
userService.changePassword(user, newPassword);
// Logout using Spring's utility
new SecurityContextLogoutHandler().logout(request, response, null);
return new RedirectView("/login?messageType=credsUpdated");
}
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
@PostMapping("/updateUserSettings")
public String updateUserSettings(HttpServletRequest request, Principal principal) {
Map<String, String[]> paramMap = request.getParameterMap();
Map<String, String> updates = new HashMap<>();
System.out.println("Received parameter map: " + paramMap);
for (Map.Entry<String, String[]> entry : paramMap.entrySet()) {
updates.put(entry.getKey(), entry.getValue()[0]);
}
System.out.println("Processed updates: " + updates);
// Assuming you have a method in userService to update the settings for a user
userService.updateUserSettings(principal.getName(), updates);
return "redirect:/account"; // Redirect to a page of your choice after updating
}
@PreAuthorize("hasRole('ROLE_ADMIN')")
@PostMapping("/admin/saveUser")
public RedirectView saveUser(@RequestParam String username, @RequestParam String password, @RequestParam String role,
@RequestParam(name = "forceChange", required = false, defaultValue = "false") boolean forceChange) {
if(userService.usernameExists(username)) {
return new RedirectView("/addUsers?messageType=usernameExists");
}
try {
// Validate the role
Role roleEnum = Role.fromString(role);
if (roleEnum == Role.INTERNAL_API_USER) {
// If the role is INTERNAL_API_USER, reject the request
return new RedirectView("/addUsers?messageType=invalidRole");
}
} catch (IllegalArgumentException e) {
// If the role ID is not valid, redirect with an error message
return new RedirectView("/addUsers?messageType=invalidRole");
}
userService.saveUser(username, password, role, forceChange);
return new RedirectView("/addUsers"); // Redirect to account page after adding the user
}
@PreAuthorize("hasRole('ROLE_ADMIN')")
@PostMapping("/admin/deleteUser/{username}")
public String deleteUser(@PathVariable String username, Authentication authentication) {
// Get the currently authenticated username
String currentUsername = authentication.getName();
// Check if the provided username matches the current session's username
if (currentUsername.equals(username)) {
throw new IllegalArgumentException("Cannot delete currently logined in user.");
}
userService.deleteUser(username);
return "redirect:/addUsers";
}
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
@PostMapping("/get-api-key")
public ResponseEntity<String> getApiKey(Principal principal) {
if (principal == null) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("User not authenticated.");
}
String username = principal.getName();
String apiKey = userService.getApiKeyForUser(username);
if (apiKey == null) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body("API key not found for user.");
}
return ResponseEntity.ok(apiKey);
}
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
@PostMapping("/update-api-key")
public ResponseEntity<String> updateApiKey(Principal principal) {
if (principal == null) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("User not authenticated.");
}
String username = principal.getName();
User user = userService.refreshApiKeyForUser(username);
String apiKey = user.getApiKey();
if (apiKey == null) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body("API key not found for user.");
}
return ResponseEntity.ok(apiKey);
}
}

View File

@@ -1,139 +0,0 @@
package stirling.software.SPDF.controller.api.converters;
import java.io.IOException;
import org.commonmark.node.Node;
import org.commonmark.parser.Parser;
import org.commonmark.renderer.html.HtmlRenderer;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.utils.FileToPdf;
import stirling.software.SPDF.utils.WebResponseUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.http.ResponseEntity;
import io.swagger.v3.oas.annotations.Operation;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.DocumentBuilder;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
@RestController
@Tag(name = "Convert", description = "Convert APIs")
public class ConvertEpubToPdf {
//TODO
@PostMapping(consumes = "multipart/form-data", value = "/epub-to-single-pdf")
@Hidden
@Operation(
summary = "Convert an EPUB file to a single PDF",
description = "This endpoint takes an EPUB file input and converts it to a single PDF."
)
public ResponseEntity<byte[]> epubToSinglePdf(
@RequestPart(required = true, value = "fileInput") MultipartFile fileInput)
throws Exception {
if (fileInput == null) {
throw new IllegalArgumentException("Please provide an EPUB file for conversion.");
}
String originalFilename = fileInput.getOriginalFilename();
if (originalFilename == null || !originalFilename.endsWith(".epub")) {
throw new IllegalArgumentException("File must be in .epub format.");
}
Map<String, byte[]> epubContents = extractEpubContent(fileInput);
List<String> htmlFilesOrder = getHtmlFilesOrderFromOpf(epubContents);
List<byte[]> individualPdfs = new ArrayList<>();
for (String htmlFile : htmlFilesOrder) {
byte[] htmlContent = epubContents.get(htmlFile);
byte[] pdfBytes = FileToPdf.convertHtmlToPdf(htmlContent, htmlFile.replace(".html", ".pdf"));
individualPdfs.add(pdfBytes);
}
// Pseudo-code to merge individual PDFs into one.
byte[] mergedPdfBytes = mergeMultiplePdfsIntoOne(individualPdfs);
return WebResponseUtils.bytesToWebResponse(mergedPdfBytes, originalFilename.replace(".epub", ".pdf"));
}
// Assuming a pseudo-code function that merges multiple PDFs into one.
private byte[] mergeMultiplePdfsIntoOne(List<byte[]> individualPdfs) {
// You can use a library such as iText or PDFBox to perform the merging here.
// Return the byte[] of the merged PDF.
return null;
}
private Map<String, byte[]> extractEpubContent(MultipartFile fileInput) throws IOException {
Map<String, byte[]> contentMap = new HashMap<>();
try (ZipInputStream zis = new ZipInputStream(fileInput.getInputStream())) {
ZipEntry zipEntry = zis.getNextEntry();
while (zipEntry != null) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int read = 0;
while ((read = zis.read(buffer)) != -1) {
baos.write(buffer, 0, read);
}
contentMap.put(zipEntry.getName(), baos.toByteArray());
zipEntry = zis.getNextEntry();
}
}
return contentMap;
}
private List<String> getHtmlFilesOrderFromOpf(Map<String, byte[]> epubContents) throws Exception {
String opfContent = new String(epubContents.get("OEBPS/content.opf")); // Adjusting for given path
DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
InputSource is = new InputSource(new StringReader(opfContent));
Document doc = dBuilder.parse(is);
NodeList itemRefs = doc.getElementsByTagName("itemref");
List<String> htmlFilesOrder = new ArrayList<>();
for (int i = 0; i < itemRefs.getLength(); i++) {
Element itemRef = (Element) itemRefs.item(i);
String idref = itemRef.getAttribute("idref");
NodeList items = doc.getElementsByTagName("item");
for (int j = 0; j < items.getLength(); j++) {
Element item = (Element) items.item(j);
if (idref.equals(item.getAttribute("id"))) {
htmlFilesOrder.add(item.getAttribute("href")); // Fetching the actual href
break;
}
}
}
return htmlFilesOrder;
}
}

View File

@@ -1,42 +1,33 @@
package stirling.software.SPDF.controller.api.converters;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
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.RequestPart;
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.GeneralFile;
import stirling.software.SPDF.utils.FileToPdf;
import stirling.software.SPDF.utils.GeneralUtils;
import stirling.software.SPDF.utils.ProcessExecutor;
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@Tag(name = "Convert", description = "Convert APIs")
@RequestMapping("/api/v1/convert")
public class ConvertHtmlToPDF {
@PostMapping(consumes = "multipart/form-data", value = "/html-to-pdf")
@PostMapping(consumes = "multipart/form-data", value = "/html/pdf")
@Operation(
summary = "Convert an HTML or ZIP (containing HTML and CSS) to PDF",
description = "This endpoint takes an HTML or ZIP file input and converts it to a PDF format."
)
public ResponseEntity<byte[]> HtmlToPdf(
@RequestPart(required = true, value = "fileInput") MultipartFile fileInput) throws IOException, InterruptedException {
@ModelAttribute GeneralFile request)
throws Exception {
MultipartFile fileInput = request.getFileInput();
if (fileInput == null) {
throw new IllegalArgumentException("Please provide an HTML or ZIP file for conversion.");

View File

@@ -1,6 +1,7 @@
package stirling.software.SPDF.controller.api.converters;
import java.io.IOException;
import java.net.URLConnection;
import org.apache.pdfbox.rendering.ImageType;
import org.slf4j.Logger;
@@ -11,44 +12,36 @@ import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
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.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.converters.ConvertToImageRequest;
import stirling.software.SPDF.model.api.converters.ConvertToPdfRequest;
import stirling.software.SPDF.utils.PdfUtils;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@RequestMapping("/api/v1/convert")
@Tag(name = "Convert", description = "Convert APIs")
public class ConvertImgPDFController {
private static final Logger logger = LoggerFactory.getLogger(ConvertImgPDFController.class);
@PostMapping(consumes = "multipart/form-data", value = "/pdf-to-img")
@PostMapping(consumes = "multipart/form-data", value = "/pdf/img")
@Operation(summary = "Convert PDF to image(s)",
description = "This endpoint converts a PDF file to image(s) with the specified image format, color type, and DPI. Users can choose to get a single image or multiple images. Input:PDF Output:Image Type:SI-Conditional")
public ResponseEntity<Resource> convertToImage(
@RequestPart(required = true, value = "fileInput")
@Parameter(description = "The input PDF file to be converted")
MultipartFile file,
@RequestParam("imageFormat")
@Parameter(description = "The output image format", schema = @Schema(allowableValues = {"png", "jpeg", "jpg", "gif"}))
String imageFormat,
@RequestParam("singleOrMultiple")
@Parameter(description = "Choose between a single image containing all pages or separate images for each page", schema = @Schema(allowableValues = {"single", "multiple"}))
String singleOrMultiple,
@RequestParam("colorType")
@Parameter(description = "The color type of the output image(s)", schema = @Schema(allowableValues = {"color", "greyscale", "blackwhite"}))
String colorType,
@RequestParam("dpi")
@Parameter(description = "The DPI (dots per inch) for the output image(s)")
String dpi) throws IOException {
public ResponseEntity<Resource> convertToImage(@ModelAttribute ConvertToImageRequest request) throws IOException {
MultipartFile file = request.getFileInput();
String imageFormat = request.getImageFormat();
String singleOrMultiple = request.getSingleOrMultiple();
String colorType = request.getColorType();
String dpi = request.getDpi();
byte[] pdfBytes = file.getBytes();
ImageType colorTypeResult = ImageType.RGB;
if ("greyscale".equals(colorType)) {
@@ -83,37 +76,22 @@ public class ConvertImgPDFController {
}
}
@PostMapping(consumes = "multipart/form-data", value = "/img-to-pdf")
@PostMapping(consumes = "multipart/form-data", value = "/img/pdf")
@Operation(summary = "Convert images to a PDF file",
description = "This endpoint converts one or more images to a PDF file. Users can specify whether to stretch the images to fit the PDF page, and whether to automatically rotate the images. Input:Image Output:PDF Type:SISO?")
public ResponseEntity<byte[]> convertToPdf(
@RequestPart(required = true, value = "fileInput")
@Parameter(description = "The input images to be converted to a PDF file")
MultipartFile[] file,
@RequestParam(defaultValue = "false", name = "stretchToFit")
@Parameter(description = "Whether to stretch the images to fit the PDF page or maintain the aspect ratio", example = "false")
boolean stretchToFit,
@RequestParam("colorType")
@Parameter(description = "The color type of the output image(s)", schema = @Schema(allowableValues = {"color", "greyscale", "blackwhite"}))
String colorType,
@RequestParam(defaultValue = "false", name = "autoRotate")
@Parameter(description = "Whether to automatically rotate the images to better fit the PDF page", example = "true")
boolean autoRotate) throws IOException {
public ResponseEntity<byte[]> convertToPdf(@ModelAttribute ConvertToPdfRequest request) throws IOException {
MultipartFile[] file = request.getFileInput();
String fitOption = request.getFitOption();
String colorType = request.getColorType();
boolean autoRotate = request.isAutoRotate();
// Convert the file to PDF and get the resulting bytes
byte[] bytes = PdfUtils.imageToPdf(file, stretchToFit, autoRotate, colorType);
byte[] bytes = PdfUtils.imageToPdf(file, fitOption, autoRotate, colorType);
return WebResponseUtils.bytesToWebResponse(bytes, file[0].getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_converted.pdf");
}
private String getMediaType(String imageFormat) {
if (imageFormat.equalsIgnoreCase("PNG"))
return "image/png";
else if (imageFormat.equalsIgnoreCase("JPEG") || imageFormat.equalsIgnoreCase("JPG"))
return "image/jpeg";
else if (imageFormat.equalsIgnoreCase("GIF"))
return "image/gif";
else
return "application/octet-stream";
String mimeType = URLConnection.guessContentTypeFromName("." + imageFormat);
return mimeType.equals("null") ? "application/octet-stream" : mimeType;
}
}

View File

@@ -1,33 +1,35 @@
package stirling.software.SPDF.controller.api.converters;
import java.io.IOException;
import org.commonmark.node.Node;
import org.commonmark.parser.Parser;
import org.commonmark.renderer.html.HtmlRenderer;
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.RequestPart;
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.GeneralFile;
import stirling.software.SPDF.utils.FileToPdf;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@Tag(name = "Convert", description = "Convert APIs")
@RequestMapping("/api/v1/convert")
public class ConvertMarkdownToPdf {
@PostMapping(consumes = "multipart/form-data", value = "/markdown-to-pdf")
@PostMapping(consumes = "multipart/form-data", value = "/markdown/pdf")
@Operation(
summary = "Convert a Markdown file to PDF",
description = "This endpoint takes a Markdown file input, converts it to HTML, and then to PDF format."
)
public ResponseEntity<byte[]> markdownToPdf(
@RequestPart(required = true, value = "fileInput") MultipartFile fileInput)
throws IOException, InterruptedException {
@ModelAttribute GeneralFile request)
throws Exception {
MultipartFile fileInput = request.getFileInput();
if (fileInput == null) {
throw new IllegalArgumentException("Please provide a Markdown file for conversion.");

View File

@@ -10,20 +10,22 @@ import java.util.List;
import org.apache.commons.io.FilenameUtils;
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.RequestPart;
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.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.GeneralFile;
import stirling.software.SPDF.utils.ProcessExecutor;
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@Tag(name = "Convert", description = "Convert APIs")
@RequestMapping("/api/v1/convert")
public class ConvertOfficeController {
public byte[] convertToPdf(MultipartFile inputFile) throws IOException, InterruptedException {
@@ -58,19 +60,14 @@ public class ConvertOfficeController {
return fileExtension.matches(extensionPattern);
}
@PostMapping(consumes = "multipart/form-data", value = "/file-to-pdf")
@PostMapping(consumes = "multipart/form-data", value = "/file/pdf")
@Operation(
summary = "Convert a file to a PDF using LibreOffice",
description = "This endpoint converts a given file to a PDF using LibreOffice API Input:Any Output:PDF Type:SISO"
)
public ResponseEntity<byte[]> processFileToPDF(
@RequestPart(required = true, value = "fileInput")
@Parameter(
description = "The input file to be converted to a PDF file using LibreOffice",
required = true
)
MultipartFile inputFile
) throws IOException, InterruptedException {
public ResponseEntity<byte[]> processFileToPDF(@ModelAttribute GeneralFile request)
throws Exception {
MultipartFile inputFile = request.getFileInput();
// unused but can start server instance if startup time is to long
// LibreOfficeListener.getInstance().start();

View File

@@ -3,69 +3,67 @@ package stirling.software.SPDF.controller.api.converters;
import java.io.IOException;
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.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
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.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.PDFFile;
import stirling.software.SPDF.model.api.converters.PdfToPresentationRequest;
import stirling.software.SPDF.model.api.converters.PdfToTextOrRTFRequest;
import stirling.software.SPDF.model.api.converters.PdfToWordRequest;
import stirling.software.SPDF.utils.PDFToFile;
@RestController
@RequestMapping("/api/v1/convert")
@Tag(name = "Convert", description = "Convert APIs")
public class ConvertPDFToOffice {
@PostMapping(consumes = "multipart/form-data", value = "/pdf-to-html")
@PostMapping(consumes = "multipart/form-data", value = "/pdf/html")
@Operation(summary = "Convert PDF to HTML", description = "This endpoint converts a PDF file to HTML format. Input:PDF Output:HTML Type:SISO")
public ResponseEntity<byte[]> processPdfToHTML(
@RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file to be converted to HTML format", required = true) MultipartFile inputFile)
throws IOException, InterruptedException {
public ResponseEntity<byte[]> processPdfToHTML(@ModelAttribute PDFFile request)
throws Exception {
MultipartFile inputFile = request.getFileInput();
PDFToFile pdfToFile = new PDFToFile();
return pdfToFile.processPdfToOfficeFormat(inputFile, "html", "writer_pdf_import");
}
@PostMapping(consumes = "multipart/form-data", value = "/pdf-to-presentation")
@PostMapping(consumes = "multipart/form-data", value = "/pdf/presentation")
@Operation(summary = "Convert PDF to Presentation format", description = "This endpoint converts a given PDF file to a Presentation format. Input:PDF Output:PPT Type:SISO")
public ResponseEntity<byte[]> processPdfToPresentation(
@RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file") MultipartFile inputFile,
@RequestParam("outputFormat") @Parameter(description = "The output Presentation format", schema = @Schema(allowableValues = {
"ppt", "pptx", "odp" })) String outputFormat)
throws IOException, InterruptedException {
public ResponseEntity<byte[]> processPdfToPresentation(@ModelAttribute PdfToPresentationRequest request) throws IOException, InterruptedException {
MultipartFile inputFile = request.getFileInput();
String outputFormat = request.getOutputFormat();
PDFToFile pdfToFile = new PDFToFile();
return pdfToFile.processPdfToOfficeFormat(inputFile, outputFormat, "impress_pdf_import");
}
@PostMapping(consumes = "multipart/form-data", value = "/pdf-to-text")
@PostMapping(consumes = "multipart/form-data", value = "/pdf/text")
@Operation(summary = "Convert PDF to Text or RTF format", description = "This endpoint converts a given PDF file to Text or RTF format. Input:PDF Output:TXT Type:SISO")
public ResponseEntity<byte[]> processPdfToRTForTXT(
@RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file") MultipartFile inputFile,
@RequestParam("outputFormat") @Parameter(description = "The output Text or RTF format", schema = @Schema(allowableValues = {
"rtf", "txt:Text" })) String outputFormat)
throws IOException, InterruptedException {
public ResponseEntity<byte[]> processPdfToRTForTXT(@ModelAttribute PdfToTextOrRTFRequest request) throws IOException, InterruptedException {
MultipartFile inputFile = request.getFileInput();
String outputFormat = request.getOutputFormat();
PDFToFile pdfToFile = new PDFToFile();
return pdfToFile.processPdfToOfficeFormat(inputFile, outputFormat, "writer_pdf_import");
}
@PostMapping(consumes = "multipart/form-data", value = "/pdf-to-word")
@PostMapping(consumes = "multipart/form-data", value = "/pdf/word")
@Operation(summary = "Convert PDF to Word document", description = "This endpoint converts a given PDF file to a Word document format. Input:PDF Output:WORD Type:SISO")
public ResponseEntity<byte[]> processPdfToWord(
@RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file") MultipartFile inputFile,
@RequestParam("outputFormat") @Parameter(description = "The output Word document format", schema = @Schema(allowableValues = {
"doc", "docx", "odt" })) String outputFormat)
throws IOException, InterruptedException {
public ResponseEntity<byte[]> processPdfToWord(@ModelAttribute PdfToWordRequest request) throws IOException, InterruptedException {
MultipartFile inputFile = request.getFileInput();
String outputFormat = request.getOutputFormat();
PDFToFile pdfToFile = new PDFToFile();
return pdfToFile.processPdfToOfficeFormat(inputFile, outputFormat, "writer_pdf_import");
}
@PostMapping(consumes = "multipart/form-data", value = "/pdf-to-xml")
@PostMapping(consumes = "multipart/form-data", value = "/pdf/xml")
@Operation(summary = "Convert PDF to XML", description = "This endpoint converts a PDF file to an XML file. Input:PDF Output:XML Type:SISO")
public ResponseEntity<byte[]> processPdfToXML(
@RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file to be converted to an XML file", required = true) MultipartFile inputFile)
throws IOException, InterruptedException {
public ResponseEntity<byte[]> processPdfToXML(@ModelAttribute PDFFile request)
throws Exception {
MultipartFile inputFile = request.getFileInput();
PDFToFile pdfToFile = new PDFToFile();
return pdfToFile.processPdfToOfficeFormat(inputFile, "xml", "writer_pdf_import");

View File

@@ -1,37 +1,37 @@
package stirling.software.SPDF.controller.api.converters;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
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.RequestPart;
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.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.PDFFile;
import stirling.software.SPDF.utils.ProcessExecutor;
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@RequestMapping("/api/v1/convert")
@Tag(name = "Convert", description = "Convert APIs")
public class ConvertPDFToPDFA {
@PostMapping(consumes = "multipart/form-data", value = "/pdf-to-pdfa")
@PostMapping(consumes = "multipart/form-data", value = "/pdf/pdfa")
@Operation(
summary = "Convert a PDF to a PDF/A",
description = "This endpoint converts a PDF file to a PDF/A file. PDF/A is a format designed for long-term archiving of digital documents. Input:PDF Output:PDF Type:SISO"
)
public ResponseEntity<byte[]> pdfToPdfA(
@RequestPart(required = true, value = "fileInput")
@Parameter(description = "The input PDF file to be converted to a PDF/A file", required = true)
MultipartFile inputFile) throws IOException, InterruptedException {
public ResponseEntity<byte[]> pdfToPdfA(@ModelAttribute PDFFile request)
throws Exception {
MultipartFile inputFile = request.getFileInput();
// Save the uploaded file to a temporary location
Path tempInputFile = Files.createTempFile("input_", ".pdf");

View File

@@ -7,15 +7,14 @@ import java.util.ArrayList;
import java.util.List;
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.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
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.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.converters.UrlToPdfRequest;
import stirling.software.SPDF.utils.GeneralUtils;
import stirling.software.SPDF.utils.ProcessExecutor;
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
@@ -23,17 +22,16 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@Tag(name = "Convert", description = "Convert APIs")
@RequestMapping("/api/v1/convert")
public class ConvertWebsiteToPDF {
@PostMapping(consumes = "multipart/form-data", value = "/url-to-pdf")
@PostMapping(consumes = "multipart/form-data", value = "/url/pdf")
@Operation(
summary = "Convert a URL to a PDF",
description = "This endpoint fetches content from a URL and converts it to a PDF format."
description = "This endpoint fetches content from a URL and converts it to a PDF format. Input:N/A Output:PDF Type:SISO"
)
public ResponseEntity<byte[]> urlToPdf(
@RequestParam(required = true, value = "urlInput")
@Parameter(description = "The input URL to be converted to a PDF file", required = true)
String URL) throws IOException, InterruptedException {
public ResponseEntity<byte[]> urlToPdf(@ModelAttribute UrlToPdfRequest request) throws IOException, InterruptedException {
String URL = request.getUrlInput();
// Validate the URL format
if(!URL.matches("^https?://.*") || !GeneralUtils.isValidURL(URL)) {

View File

@@ -0,0 +1,123 @@
package stirling.software.SPDF.controller.api.converters;
import java.io.ByteArrayInputStream;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.List;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ContentDisposition;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.opencsv.CSVWriter;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.controller.api.CropController;
import stirling.software.SPDF.controller.api.strippers.PDFTableStripper;
import stirling.software.SPDF.model.api.extract.PDFFilePage;
@RestController
@RequestMapping("/api/v1/convert")
@Tag(name = "Convert", description = "Convert APIs")
public class ExtractController {
private static final Logger logger = LoggerFactory.getLogger(CropController.class);
@PostMapping(value = "/pdf/csv", consumes = "multipart/form-data")
@Operation(summary = "Extracts a PDF document to csv", description = "This operation takes an input PDF file and returns CSV file of whole page. Input:PDF Output:CSV Type:SISO")
public ResponseEntity<String> PdfToCsv(@ModelAttribute PDFFilePage form)
throws Exception {
ArrayList<String> tableData = new ArrayList<>();
int columnsCount = 0;
try (PDDocument document = PDDocument.load(new ByteArrayInputStream(form.getFileInput().getBytes()))) {
final double res = 72; // PDF units are at 72 DPI
PDFTableStripper stripper = new PDFTableStripper();
PDPage pdPage = document.getPage(form.getPageId() - 1);
stripper.extractTable(pdPage);
columnsCount = stripper.getColumns();
for (int c = 0; c < columnsCount; ++c) {
for(int r=0; r<stripper.getRows(); ++r) {
tableData.add(stripper.getText(r, c));
}
}
}
ArrayList<String> notEmptyColumns = new ArrayList<>();
for (String item: tableData) {
if(!item.trim().isEmpty()){
notEmptyColumns.add(item);
}else{
columnsCount--;
}
}
List<String> fullTable = notEmptyColumns.stream().map((entity)->
entity.replace('\n',' ').replace('\r',' ').trim().replaceAll("\\s{2,}", "|")).toList();
int rowsCount = fullTable.get(0).split("\\|").length;
ArrayList<String> headersList = getTableHeaders(columnsCount,fullTable);
ArrayList<String> recordList = getRecordsList(rowsCount,fullTable);
if(headersList.size() == 0 && recordList.size() == 0) {
throw new Exception("No table detected, no headers or records found");
}
StringWriter writer = new StringWriter();
try (CSVWriter csvWriter = new CSVWriter(writer)) {
csvWriter.writeNext(headersList.toArray(new String[0]));
for (String record : recordList) {
csvWriter.writeNext(record.split("\\|"));
}
}
HttpHeaders headers = new HttpHeaders();
headers.setContentDisposition(ContentDisposition.builder("attachment").filename(form.getFileInput().getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_extracted.csv").build());
headers.setContentType(MediaType.parseMediaType("text/csv"));
return ResponseEntity.ok()
.headers(headers)
.body(writer.toString());
}
private ArrayList<String> getRecordsList( int rowsCounts ,List<String> items){
ArrayList<String> recordsList = new ArrayList<>();
for (int b=1; b<rowsCounts;b++) {
StringBuilder strbldr = new StringBuilder();
for (int i=0;i<items.size();i++){
String[] parts = items.get(i).split("\\|");
strbldr.append(parts[b]);
if (i!= items.size()-1){
strbldr.append("|");
}
}
recordsList.add(strbldr.toString());
}
return recordsList;
}
private ArrayList<String> getTableHeaders(int columnsCount, List<String> items){
ArrayList<String> resultList = new ArrayList<>();
for (int i=0;i<columnsCount;i++){
String[] parts = items.get(i).split("\\|");
resultList.add(parts[0]);
}
return resultList;
}
}

View File

@@ -1,39 +1,40 @@
package stirling.software.SPDF.controller.api.filters;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
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.RequestPart;
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.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.PDFComparisonAndCount;
import stirling.software.SPDF.model.api.PDFWithPageNums;
import stirling.software.SPDF.model.api.filter.ContainsTextRequest;
import stirling.software.SPDF.model.api.filter.FileSizeRequest;
import stirling.software.SPDF.model.api.filter.PageRotationRequest;
import stirling.software.SPDF.model.api.filter.PageSizeRequest;
import stirling.software.SPDF.utils.PdfUtils;
import stirling.software.SPDF.utils.ProcessExecutor;
import stirling.software.SPDF.utils.WebResponseUtils;
import io.swagger.v3.oas.annotations.media.Schema;
@RestController
@RequestMapping("/api/v1/filter")
@Tag(name = "Filter", description = "Filter APIs")
public class FilterController {
@PostMapping(consumes = "multipart/form-data", value = "/filter-contains-text")
@Operation(summary = "Checks if a PDF contains set text, returns true if does", description = "Input:PDF Output:Boolean Type:SISO")
public ResponseEntity<byte[]> containsText(
@RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file to be converted to a PDF/A file", required = true) MultipartFile inputFile,
@Parameter(description = "The text to check for", required = true) String text,
@Parameter(description = "The page number to check for text on accepts 'All', ranges like '1-4'", required = false) String pageNumber)
throws IOException, InterruptedException {
public ResponseEntity<byte[]> containsText(@ModelAttribute ContainsTextRequest request) throws IOException, InterruptedException {
MultipartFile inputFile = request.getFileInput();
String text = request.getText();
String pageNumber = request.getPageNumbers();
PDDocument pdfDocument = PDDocument.load(inputFile.getInputStream());
if (PdfUtils.hasText(pdfDocument, pageNumber, text))
return WebResponseUtils.pdfDocToWebResponse(pdfDocument, inputFile.getOriginalFilename());
@@ -43,10 +44,11 @@ public class FilterController {
// TODO
@PostMapping(consumes = "multipart/form-data", value = "/filter-contains-image")
@Operation(summary = "Checks if a PDF contains an image", description = "Input:PDF Output:Boolean Type:SISO")
public ResponseEntity<byte[]> containsImage(
@RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file to be converted to a PDF/A file", required = true) MultipartFile inputFile,
@Parameter(description = "The page number to check for image on accepts 'All', ranges like '1-4'", required = false) String pageNumber)
public ResponseEntity<byte[]> containsImage(@ModelAttribute PDFWithPageNums request)
throws IOException, InterruptedException {
MultipartFile inputFile = request.getFileInput();
String pageNumber = request.getPageNumbers();
PDDocument pdfDocument = PDDocument.load(inputFile.getInputStream());
if (PdfUtils.hasImages(pdfDocument, pageNumber))
return WebResponseUtils.pdfDocToWebResponse(pdfDocument, inputFile.getOriginalFilename());
@@ -55,12 +57,10 @@ public class FilterController {
@PostMapping(consumes = "multipart/form-data", value = "/filter-page-count")
@Operation(summary = "Checks if a PDF is greater, less or equal to a setPageCount", description = "Input:PDF Output:Boolean Type:SISO")
public ResponseEntity<byte[]> pageCount(
@RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file", required = true) MultipartFile inputFile,
@Parameter(description = "Page Count", required = true) String pageCount,
@Parameter(description = "Comparison type", schema = @Schema(description = "The comparison type, accepts Greater, Equal, Less than", allowableValues = {
"Greater", "Equal", "Less" })) String comparator)
throws IOException, InterruptedException {
public ResponseEntity<byte[]> pageCount(@ModelAttribute PDFComparisonAndCount request) throws IOException, InterruptedException {
MultipartFile inputFile = request.getFileInput();
String pageCount = request.getPageCount();
String comparator = request.getComparator();
// Load the PDF
PDDocument document = PDDocument.load(inputFile.getInputStream());
int actualPageCount = document.getNumberOfPages();
@@ -88,12 +88,10 @@ public class FilterController {
@PostMapping(consumes = "multipart/form-data", value = "/filter-page-size")
@Operation(summary = "Checks if a PDF is of a certain size", description = "Input:PDF Output:Boolean Type:SISO")
public ResponseEntity<byte[]> pageSize(
@RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file", required = true) MultipartFile inputFile,
@Parameter(description = "Standard Page Size", required = true) String standardPageSize,
@Parameter(description = "Comparison type", schema = @Schema(description = "The comparison type, accepts Greater, Equal, Less than", allowableValues = {
"Greater", "Equal", "Less" })) String comparator)
throws IOException, InterruptedException {
public ResponseEntity<byte[]> pageSize(@ModelAttribute PageSizeRequest request) throws IOException, InterruptedException {
MultipartFile inputFile = request.getFileInput();
String standardPageSize = request.getStandardPageSize();
String comparator = request.getComparator();
// Load the PDF
PDDocument document = PDDocument.load(inputFile.getInputStream());
@@ -131,12 +129,10 @@ public class FilterController {
@PostMapping(consumes = "multipart/form-data", value = "/filter-file-size")
@Operation(summary = "Checks if a PDF is a set file size", description = "Input:PDF Output:Boolean Type:SISO")
public ResponseEntity<byte[]> fileSize(
@RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file", required = true) MultipartFile inputFile,
@Parameter(description = "File Size", required = true) String fileSize,
@Parameter(description = "Comparison type", schema = @Schema(description = "The comparison type, accepts Greater, Equal, Less than", allowableValues = {
"Greater", "Equal", "Less" })) String comparator)
throws IOException, InterruptedException {
public ResponseEntity<byte[]> fileSize(@ModelAttribute FileSizeRequest request) throws IOException, InterruptedException {
MultipartFile inputFile = request.getFileInput();
String fileSize = request.getFileSize();
String comparator = request.getComparator();
// Get the file size
long actualFileSize = inputFile.getSize();
@@ -164,12 +160,10 @@ public class FilterController {
@PostMapping(consumes = "multipart/form-data", value = "/filter-page-rotation")
@Operation(summary = "Checks if a PDF is of a certain rotation", description = "Input:PDF Output:Boolean Type:SISO")
public ResponseEntity<byte[]> pageRotation(
@RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file", required = true) MultipartFile inputFile,
@Parameter(description = "Rotation in degrees", required = true) int rotation,
@Parameter(description = "Comparison type", schema = @Schema(description = "The comparison type, accepts Greater, Equal, Less than", allowableValues = {
"Greater", "Equal", "Less" })) String comparator)
throws IOException, InterruptedException {
public ResponseEntity<byte[]> pageRotation(@ModelAttribute PageRotationRequest request) throws IOException, InterruptedException {
MultipartFile inputFile = request.getFileInput();
int rotation = request.getRotation();
String comparator = request.getComparator();
// Load the PDF
PDDocument document = PDDocument.load(inputFile.getInputStream());

View File

@@ -1,77 +1,29 @@
package stirling.software.SPDF.controller.api.other;
package stirling.software.SPDF.controller.api.misc;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.text.PDFTextStripper;
import org.apache.pdfbox.text.TextPosition;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
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.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
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.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.utils.GeneralUtils;
import stirling.software.SPDF.utils.PdfUtils;
import stirling.software.SPDF.model.api.misc.ExtractHeaderRequest;
import stirling.software.SPDF.utils.WebResponseUtils;
import org.apache.pdfbox.pdmodel.*;
import org.apache.pdfbox.pdmodel.common.*;
import org.apache.pdfbox.pdmodel.PDPageContentStream.*;
import org.springframework.web.bind.annotation.*;
import org.springframework.http.*;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.*;
import io.swagger.v3.oas.annotations.media.*;
import io.swagger.v3.oas.annotations.parameters.*;
import org.apache.pdfbox.pdmodel.font.PDType1Font;
import org.apache.pdfbox.text.TextPosition;
import org.apache.tomcat.util.http.ResponseUtil;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.URLEncoder;
import java.util.List;
import java.util.ArrayList;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
import com.itextpdf.io.font.constants.StandardFonts;
import com.itextpdf.kernel.font.PdfFont;
import com.itextpdf.kernel.font.PdfFontFactory;
import com.itextpdf.kernel.geom.Rectangle;
import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfPage;
import com.itextpdf.kernel.pdf.canvas.PdfCanvas;
import com.itextpdf.layout.Canvas;
import com.itextpdf.layout.element.Paragraph;
import com.itextpdf.layout.properties.TextAlignment;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.*;
import org.apache.pdfbox.pdmodel.*;
import org.apache.pdfbox.text.*;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.*;
import io.swagger.v3.oas.annotations.media.Schema;
import org.springframework.http.ResponseEntity;
@RestController
@Tag(name = "Other", description = "Other APIs")
@RequestMapping("/api/v1/misc")
@Tag(name = "Misc", description = "Miscellaneous APIs")
public class AutoRenameController {
private static final Logger logger = LoggerFactory.getLogger(AutoRenameController.class);
@@ -81,10 +33,9 @@ public class AutoRenameController {
@PostMapping(consumes = "multipart/form-data", value = "/auto-rename")
@Operation(summary = "Extract header from PDF file", description = "This endpoint accepts a PDF file and attempts to extract its title or header based on heuristics. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> extractHeader(
@RequestPart(value = "fileInput") @Parameter(description = "The input PDF file from which the header is to be extracted.", required = true) MultipartFile file,
@RequestParam(required = false, defaultValue = "false") @Parameter(description = "Flag indicating whether to use the first text as a fallback if no suitable title is found. Defaults to false.", required = false) Boolean useFirstTextAsFallback)
throws Exception {
public ResponseEntity<byte[]> extractHeader(@ModelAttribute ExtractHeaderRequest request) throws Exception {
MultipartFile file = request.getFileInput();
Boolean useFirstTextAsFallback = request.isUseFirstTextAsFallback();
PDDocument document = PDDocument.load(file.getInputStream());
PDFTextStripper reader = new PDFTextStripper() {

View File

@@ -1,4 +1,4 @@
package stirling.software.SPDF.controller.api.other;
package stirling.software.SPDF.controller.api.misc;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferByte;
import java.awt.image.DataBufferInt;
@@ -16,8 +16,9 @@ import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.rendering.PDFRenderer;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
@@ -29,21 +30,23 @@ import com.google.zxing.PlanarYUVLuminanceSource;
import com.google.zxing.Result;
import com.google.zxing.common.HybridBinarizer;
import stirling.software.SPDF.utils.WebResponseUtils;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.misc.AutoSplitPdfRequest;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@RequestMapping("/api/v1/misc")
@Tag(name = "Misc", description = "Miscellaneous APIs")
public class AutoSplitPdfController {
private static final String QR_CONTENT = "https://github.com/Frooodle/Stirling-PDF";
@PostMapping(value = "/auto-split-pdf", consumes = "multipart/form-data")
@Operation(summary = "Auto split PDF pages into separate documents", description = "This endpoint accepts a PDF file, scans each page for a specific QR code, and splits the document at the QR code boundaries. The output is a zip file containing each separate PDF document. Input:PDF Output:ZIP Type:SISO")
public ResponseEntity<byte[]> autoSplitPdf(
@RequestParam("fileInput") @Parameter(description = "The input PDF file which needs to be split into separate documents based on QR code boundaries.", required = true) MultipartFile file,
@RequestParam(value ="duplexMode",defaultValue = "false") @Parameter(description = "Flag indicating if the duplex mode is active, where the page after the divider also gets removed.", required = false) boolean duplexMode)
throws IOException {
@Operation(summary = "Auto split PDF pages into separate documents", description = "This endpoint accepts a PDF file, scans each page for a specific QR code, and splits the document at the QR code boundaries. The output is a zip file containing each separate PDF document. Input:PDF Output:ZIP-PDF Type:SISO")
public ResponseEntity<byte[]> autoSplitPdf(@ModelAttribute AutoSplitPdfRequest request) throws IOException {
MultipartFile file = request.getFileInput();
boolean duplexMode = request.isDuplexMode();
InputStream inputStream = file.getInputStream();
PDDocument document = PDDocument.load(inputStream);

View File

@@ -1,4 +1,4 @@
package stirling.software.SPDF.controller.api.other;
package stirling.software.SPDF.controller.api.misc;
import java.awt.image.BufferedImage;
import java.io.IOException;
@@ -20,22 +20,23 @@ import org.apache.pdfbox.rendering.PDFRenderer;
import org.apache.pdfbox.text.PDFTextStripper;
import org.springframework.http.HttpStatus;
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.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
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.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.misc.RemoveBlankPagesRequest;
import stirling.software.SPDF.utils.PdfUtils;
import stirling.software.SPDF.utils.ProcessExecutor;
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@Tag(name = "Other", description = "Other APIs")
@RequestMapping("/api/v1/misc")
@Tag(name = "Misc", description = "Miscellaneous APIs")
public class BlankPageController {
@PostMapping(consumes = "multipart/form-data", value = "/remove-blanks")
@@ -43,16 +44,10 @@ public class BlankPageController {
summary = "Remove blank pages from a PDF file",
description = "This endpoint removes blank pages from a given PDF file. Users can specify the threshold and white percentage to tune the detection of blank pages. Input:PDF Output:PDF Type:SISO"
)
public ResponseEntity<byte[]> removeBlankPages(
@RequestPart(required = true, value = "fileInput")
@Parameter(description = "The input PDF file from which blank pages will be removed", required = true)
MultipartFile inputFile,
@RequestParam(defaultValue = "10", name = "threshold")
@Parameter(description = "The threshold value to determine blank pages", example = "10")
int threshold,
@RequestParam(defaultValue = "99.9", name = "whitePercent")
@Parameter(description = "The percentage of white color on a page to consider it as blank", example = "99.9")
float whitePercent) throws IOException, InterruptedException {
public ResponseEntity<byte[]> removeBlankPages(@ModelAttribute RemoveBlankPagesRequest request) throws IOException, InterruptedException {
MultipartFile inputFile = request.getFileInput();
int threshold = request.getThreshold();
float whitePercent = request.getWhitePercent();
PDDocument document = null;
try {

View File

@@ -1,4 +1,4 @@
package stirling.software.SPDF.controller.api.other;
package stirling.software.SPDF.controller.api.misc;
import java.awt.Image;
import java.awt.image.BufferedImage;
@@ -22,35 +22,34 @@ import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
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.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.misc.OptimizePdfRequest;
import stirling.software.SPDF.utils.GeneralUtils;
import stirling.software.SPDF.utils.ProcessExecutor;
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@Tag(name = "Other", description = "Other APIs")
@RequestMapping("/api/v1/misc")
@Tag(name = "Misc", description = "Miscellaneous APIs")
public class CompressController {
private static final Logger logger = LoggerFactory.getLogger(CompressController.class);
@PostMapping(consumes = "multipart/form-data", value = "/compress-pdf")
@Operation(summary = "Optimize PDF file", description = "This endpoint accepts a PDF file and optimizes it based on the provided parameters. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> optimizePdf(
@RequestPart(value = "fileInput") @Parameter(description = "The input PDF file to be optimized.", required = true) MultipartFile inputFile,
@RequestParam(required = false, value = "optimizeLevel") @Parameter(description = "The level of optimization to apply to the PDF file. Higher values indicate greater compression but may reduce quality.", schema = @Schema(allowableValues = {
"1", "2", "3", "4", "5" })) Integer optimizeLevel,
@RequestParam(value = "expectedOutputSize", required = false) @Parameter(description = "The expected output size, e.g. '100MB', '25KB', etc.", required = false) String expectedOutputSizeString)
throws Exception {
public ResponseEntity<byte[]> optimizePdf(@ModelAttribute OptimizePdfRequest request) throws Exception {
MultipartFile inputFile = request.getFileInput();
Integer optimizeLevel = request.getOptimizeLevel();
String expectedOutputSizeString = request.getExpectedOutputSize();
if(expectedOutputSizeString == null && optimizeLevel == null) {
throw new Exception("Both expected output size and optimize level are not specified");

View File

@@ -1,4 +1,4 @@
package stirling.software.SPDF.controller.api.other;
package stirling.software.SPDF.controller.api.misc;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
@@ -24,20 +24,21 @@ import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
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.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.misc.ExtractImageScansRequest;
import stirling.software.SPDF.utils.ProcessExecutor;
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@Tag(name = "Other", description = "Other APIs")
@RequestMapping("/api/v1/misc")
@Tag(name = "Misc", description = "Miscellaneous APIs")
public class ExtractImageScansController {
private static final Logger logger = LoggerFactory.getLogger(ExtractImageScansController.class);
@@ -46,26 +47,16 @@ public class ExtractImageScansController {
@Operation(summary = "Extract image scans from an input file",
description = "This endpoint extracts image scans from a given file based on certain parameters. Users can specify angle threshold, tolerance, minimum area, minimum contour area, and border size. Input:PDF Output:IMAGE/ZIP Type:SIMO")
public ResponseEntity<byte[]> extractImageScans(
@RequestPart(required = true, value = "fileInput")
@Parameter(description = "The input file containing image scans")
MultipartFile inputFile,
@RequestParam(name = "angle_threshold", defaultValue = "5")
@Parameter(description = "The angle threshold for the image scan extraction", example = "5")
int angleThreshold,
@RequestParam(name = "tolerance", defaultValue = "20")
@Parameter(description = "The tolerance for the image scan extraction", example = "20")
int tolerance,
@RequestParam(name = "min_area", defaultValue = "8000")
@Parameter(description = "The minimum area for the image scan extraction", example = "8000")
int minArea,
@RequestParam(name = "min_contour_area", defaultValue = "500")
@Parameter(description = "The minimum contour area for the image scan extraction", example = "500")
int minContourArea,
@RequestParam(name = "border_size", defaultValue = "1")
@Parameter(description = "The border size for the image scan extraction", example = "1")
int borderSize) throws IOException, InterruptedException {
String fileName = inputFile.getOriginalFilename();
@RequestBody(
description = "Form data containing file and extraction parameters",
required = true,
content = @Content(
mediaType = "multipart/form-data",
schema = @Schema(implementation = ExtractImageScansRequest.class) // This should represent your form's structure
)
)
ExtractImageScansRequest form) throws IOException, InterruptedException {
String fileName = form.getFileInput().getOriginalFilename();
String extension = fileName.substring(fileName.lastIndexOf(".") + 1);
List<String> images = new ArrayList<>();
@@ -73,7 +64,7 @@ public class ExtractImageScansController {
// Check if input file is a PDF
if (extension.equalsIgnoreCase("pdf")) {
// Load PDF document
try (PDDocument document = PDDocument.load(new ByteArrayInputStream(inputFile.getBytes()))) {
try (PDDocument document = PDDocument.load(new ByteArrayInputStream(form.getFileInput().getBytes()))) {
PDFRenderer pdfRenderer = new PDFRenderer(document);
int pageCount = document.getNumberOfPages();
images = new ArrayList<>();
@@ -93,7 +84,7 @@ public class ExtractImageScansController {
}
} else {
Path tempInputFile = Files.createTempFile("input_", "." + extension);
Files.copy(inputFile.getInputStream(), tempInputFile, StandardCopyOption.REPLACE_EXISTING);
Files.copy(form.getFileInput().getInputStream(), tempInputFile, StandardCopyOption.REPLACE_EXISTING);
// Add input file path to images list
images.add(tempInputFile.toString());
}
@@ -109,11 +100,11 @@ public class ExtractImageScansController {
"./scripts/split_photos.py",
images.get(i),
tempDir.toString(),
"--angle_threshold", String.valueOf(angleThreshold),
"--tolerance", String.valueOf(tolerance),
"--min_area", String.valueOf(minArea),
"--min_contour_area", String.valueOf(minContourArea),
"--border_size", String.valueOf(borderSize)
"--angle_threshold", String.valueOf(form.getAngleThreshold()),
"--tolerance", String.valueOf(form.getTolerance()),
"--min_area", String.valueOf(form.getMinArea()),
"--min_contour_area", String.valueOf(form.getMinContourArea()),
"--border_size", String.valueOf(form.getBorderSize())
));

View File

@@ -1,11 +1,12 @@
package stirling.software.SPDF.controller.api.other;
package stirling.software.SPDF.controller.api.misc;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;
import java.util.zip.Deflater;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
@@ -20,19 +21,19 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
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.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.PDFWithImageFormatRequest;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@Tag(name = "Other", description = "Other APIs")
@RequestMapping("/api/v1/misc")
@Tag(name = "Misc", description = "Miscellaneous APIs")
public class ExtractImagesController {
private static final Logger logger = LoggerFactory.getLogger(ExtractImagesController.class);
@@ -40,13 +41,9 @@ public class ExtractImagesController {
@PostMapping(consumes = "multipart/form-data", value = "/extract-images")
@Operation(summary = "Extract images from a PDF file",
description = "This endpoint extracts images from a given PDF file and returns them in a zip file. Users can specify the output image format. Input:PDF Output:IMAGE/ZIP Type:SIMO")
public ResponseEntity<byte[]> extractImages(
@RequestPart(required = true, value = "fileInput")
@Parameter(description = "The input PDF file containing images")
MultipartFile file,
@RequestParam("format")
@Parameter(description = "The output image format e.g., 'png', 'jpeg', or 'gif'", schema = @Schema(allowableValues = {"png", "jpeg", "gif"}))
String format) throws IOException {
public ResponseEntity<byte[]> extractImages(@ModelAttribute PDFWithImageFormatRequest request) throws IOException {
MultipartFile file = request.getFileInput();
String format = request.getFormat();
System.out.println(System.currentTimeMillis() + "file=" + file.getName() + ", format=" + format);
PDDocument document = PDDocument.load(file.getBytes());
@@ -62,7 +59,8 @@ public class ExtractImagesController {
int imageIndex = 1;
String filename = file.getOriginalFilename().replaceFirst("[.][^.]+$", "");
int pageNum = 1;
int pageNum = 0;
Set<Integer> processedImages = new HashSet<>();
// Iterate over each page
for (PDPage page : document.getPages()) {
++pageNum;
@@ -70,7 +68,12 @@ public class ExtractImagesController {
for (COSName name : page.getResources().getXObjectNames()) {
if (page.getResources().isImageXObject(name)) {
PDImageXObject image = (PDImageXObject) page.getResources().getXObject(name);
int imageHash = image.hashCode();
if(processedImages.contains(imageHash)) {
continue; // Skip already processed images
}
processedImages.add(imageHash);
// Convert image to desired format
RenderedImage renderedImage = image.getImage();
BufferedImage bufferedImage = null;

View File

@@ -1,4 +1,4 @@
package stirling.software.SPDF.controller.api.other;
package stirling.software.SPDF.controller.api.misc;
import java.awt.Color;
import java.awt.geom.AffineTransform;
@@ -9,9 +9,11 @@ import java.awt.image.BufferedImageOp;
import java.awt.image.ConvolveOp;
import java.awt.image.Kernel;
import java.awt.image.RescaleOp;
import java.io.ByteArrayOutputStream;
//Required for file input/output
import java.io.File;
import java.io.IOException;
import java.security.SecureRandom;
//Other required classes
import java.util.Random;
@@ -29,21 +31,21 @@ import org.apache.pdfbox.rendering.PDFRenderer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.RequestPart;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import com.itextpdf.io.source.ByteArrayOutputStream;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.PDFFile;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@Tag(name = "Other", description = "Other APIs")
@RequestMapping("/api/v1/misc")
@Tag(name = "Misc", description = "Miscellaneous APIs")
public class FakeScanControllerWIP {
private static final Logger logger = LoggerFactory.getLogger(FakeScanControllerWIP.class);
@@ -55,10 +57,8 @@ public class FakeScanControllerWIP {
summary = "Repair a PDF file",
description = "This endpoint repairs a given PDF file by running Ghostscript command. The PDF is first saved to a temporary location, repaired, read back, and then returned as a response."
)
public ResponseEntity<byte[]> repairPdf(
@RequestPart(required = true, value = "fileInput")
@Parameter(description = "The input PDF file to be repaired", required = true)
MultipartFile inputFile) throws IOException, InterruptedException {
public ResponseEntity<byte[]> repairPdf(@ModelAttribute PDFFile request) throws IOException {
MultipartFile inputFile = request.getFileInput();
PDDocument document = PDDocument.load(inputFile.getBytes());
PDFRenderer pdfRenderer = new PDFRenderer(document);
@@ -86,7 +86,7 @@ public class FakeScanControllerWIP {
op.filter(sourceImage, destinationImage);
// Apply a rotation effect
double rotationRequired = Math.toRadians((new Random().nextInt(3 - 1) + 1)); // Random angle between 1 and 3 degrees
double rotationRequired = Math.toRadians((new SecureRandom().nextInt(3 - 1) + 1)); // Random angle between 1 and 3 degrees
double locationX = destinationImage.getWidth() / 2;
double locationY = destinationImage.getHeight() / 2;
AffineTransform tx = AffineTransform.getRotateInstance(rotationRequired, locationX, locationY);
@@ -104,7 +104,7 @@ public class FakeScanControllerWIP {
destinationImage = blurOp.filter(destinationImage, null);
// Add noise to the image based on the "dirtiness"
Random random = new Random();
Random random = new SecureRandom();
for (int y = 0; y < destinationImage.getHeight(); y++) {
for (int x = 0; x < destinationImage.getWidth(); x++) {
if (random.nextInt(100) < dirtiness) {

View File

@@ -1,4 +1,4 @@
package stirling.software.SPDF.controller.api.other;
package stirling.software.SPDF.controller.api.misc;
import java.io.IOException;
import java.text.ParseException;
@@ -11,19 +11,20 @@ import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDDocumentInformation;
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.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
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.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.misc.MetadataRequest;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@Tag(name = "Other", description = "Other APIs")
@RequestMapping("/api/v1/misc")
@Tag(name = "Misc", description = "Miscellaneous APIs")
public class MetadataController {
@@ -41,44 +42,28 @@ public class MetadataController {
@PostMapping(consumes = "multipart/form-data", value = "/update-metadata")
@Operation(summary = "Update metadata of a PDF file",
description = "This endpoint allows you to update the metadata of a given PDF file. You can add, modify, or delete standard and custom metadata fields. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> metadata(
@RequestPart(required = true, value = "fileInput")
@Parameter(description = "The input PDF file to update metadata")
MultipartFile pdfFile,
@RequestParam(value = "deleteAll", required = false, defaultValue = "false")
@Parameter(description = "Delete all metadata if set to true")
Boolean deleteAll,
@RequestParam(value = "author", required = false)
@Parameter(description = "The author of the document")
String author,
@RequestParam(value = "creationDate", required = false)
@Parameter(description = "The creation date of the document (format: yyyy/MM/dd HH:mm:ss)")
String creationDate,
@RequestParam(value = "creator", required = false)
@Parameter(description = "The creator of the document")
String creator,
@RequestParam(value = "keywords", required = false)
@Parameter(description = "The keywords for the document")
String keywords,
@RequestParam(value = "modificationDate", required = false)
@Parameter(description = "The modification date of the document (format: yyyy/MM/dd HH:mm:ss)")
String modificationDate,
@RequestParam(value = "producer", required = false)
@Parameter(description = "The producer of the document")
String producer,
@RequestParam(value = "subject", required = false)
@Parameter(description = "The subject of the document")
String subject,
@RequestParam(value = "title", required = false)
@Parameter(description = "The title of the document")
String title,
@RequestParam(value = "trapped", required = false)
@Parameter(description = "The trapped status of the document")
String trapped,
@Parameter(description = "Map list of key and value of custom parameters, note these must start with customKey and customValue if they are non standard")
@RequestParam Map<String, String> allRequestParams)
throws IOException {
public ResponseEntity<byte[]> metadata(@ModelAttribute MetadataRequest request) throws IOException {
// Extract PDF file from the request object
MultipartFile pdfFile = request.getFileInput();
// Extract metadata information
Boolean deleteAll = request.isDeleteAll();
String author = request.getAuthor();
String creationDate = request.getCreationDate();
String creator = request.getCreator();
String keywords = request.getKeywords();
String modificationDate = request.getModificationDate();
String producer = request.getProducer();
String subject = request.getSubject();
String title = request.getTitle();
String trapped = request.getTrapped();
// Extract additional custom parameters
Map<String, String> allRequestParams = request.getAllRequestParams();
if(allRequestParams == null) {
allRequestParams = new java.util.HashMap<String, String>();
}
// Load the PDF file into a PDDocument
PDDocument document = PDDocument.load(pdfFile.getBytes());

View File

@@ -1,4 +1,4 @@
package stirling.software.SPDF.controller.api.other;
package stirling.software.SPDF.controller.api.misc;
import java.io.File;
import java.io.FileOutputStream;
@@ -18,28 +18,28 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
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.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.misc.ProcessPdfWithOcrRequest;
import stirling.software.SPDF.utils.ProcessExecutor;
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@Tag(name = "Other", description = "Other APIs")
@RequestMapping("/api/v1/misc")
@Tag(name = "Misc", description = "Miscellaneous APIs")
public class OCRController {
private static final Logger logger = LoggerFactory.getLogger(OCRController.class);
public List<String> getAvailableTesseractLanguages() {
String tessdataDir = "/usr/share/tesseract-ocr/4.00/tessdata";
String tessdataDir = "/usr/share/tesseract-ocr/5/tessdata";
File[] files = new File(tessdataDir).listFiles();
if (files == null) {
return Collections.emptyList();
@@ -51,35 +51,16 @@ public class OCRController {
@PostMapping(consumes = "multipart/form-data", value = "/ocr-pdf")
@Operation(summary = "Process a PDF file with OCR",
description = "This endpoint processes a PDF file using OCR (Optical Character Recognition). Users can specify languages, sidecar, deskew, clean, cleanFinal, ocrType, ocrRenderType, and removeImagesAfter options. Input:PDF Output:PDF Type:SI-Conditional")
public ResponseEntity<byte[]> processPdfWithOCR(
@RequestPart(required = true, value = "fileInput")
@Parameter(description = "The input PDF file to be processed with OCR")
MultipartFile inputFile,
@RequestParam("languages")
@Parameter(description = "List of languages to use in OCR processing")
List<String> selectedLanguages,
@RequestParam(name = "sidecar", required = false)
@Parameter(description = "Include OCR text in a sidecar text file if set to true")
Boolean sidecar,
@RequestParam(name = "deskew", required = false)
@Parameter(description = "Deskew the input file if set to true")
Boolean deskew,
@RequestParam(name = "clean", required = false)
@Parameter(description = "Clean the input file if set to true")
Boolean clean,
@RequestParam(name = "clean-final", required = false)
@Parameter(description = "Clean the final output if set to true")
Boolean cleanFinal,
@RequestParam(name = "ocrType", required = false)
@Parameter(description = "Specify the OCR type, e.g., 'skip-text', 'force-ocr', or 'Normal'", schema = @Schema(allowableValues = {"skip-text", "force-ocr", "Normal"}))
String ocrType,
@RequestParam(name = "ocrRenderType", required = false, defaultValue = "hocr")
@Parameter(description = "Specify the OCR render type, either 'hocr' or 'sandwich'", schema = @Schema(allowableValues = {"hocr", "sandwich"}))
String ocrRenderType,
@RequestParam(name = "removeImagesAfter", required = false)
@Parameter(description = "Remove images from the output PDF if set to true")
Boolean removeImagesAfter) throws IOException, InterruptedException {
public ResponseEntity<byte[]> processPdfWithOCR(@ModelAttribute ProcessPdfWithOcrRequest request) throws IOException, InterruptedException {
MultipartFile inputFile = request.getFileInput();
List<String> selectedLanguages = request.getLanguages();
Boolean sidecar = request.isSidecar();
Boolean deskew = request.isDeskew();
Boolean clean = request.isClean();
Boolean cleanFinal = request.isCleanFinal();
String ocrType = request.getOcrType();
String ocrRenderType = request.getOcrRenderType();
Boolean removeImagesAfter = request.isRemoveImagesAfter();
// --output-type pdfa
if (selectedLanguages == null || selectedLanguages.isEmpty()) {
throw new IOException("Please select at least one language.");

View File

@@ -1,4 +1,4 @@
package stirling.software.SPDF.controller.api.other;
package stirling.software.SPDF.controller.api.misc;
import java.io.IOException;
@@ -6,20 +6,21 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
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.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
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.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.misc.OverlayImageRequest;
import stirling.software.SPDF.utils.PdfUtils;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@Tag(name = "Other", description = "Other APIs")
@RequestMapping("/api/v1/misc")
@Tag(name = "Misc", description = "Miscellaneous APIs")
public class OverlayImageController {
private static final Logger logger = LoggerFactory.getLogger(OverlayImageController.class);
@@ -29,22 +30,12 @@ public class OverlayImageController {
summary = "Overlay image onto a PDF file",
description = "This endpoint overlays an image onto a PDF file at the specified coordinates. The image can be overlaid on every page of the PDF if specified. Input:PDF/IMAGE Output:PDF Type:MF-SISO"
)
public ResponseEntity<byte[]> overlayImage(
@RequestPart(required = true, value = "fileInput")
@Parameter(description = "The input PDF file to overlay the image onto.", required = true)
MultipartFile pdfFile,
@RequestParam("fileInput2")
@Parameter(description = "The image file to be overlaid onto the PDF.", required = true)
MultipartFile imageFile,
@RequestParam("x")
@Parameter(description = "The x-coordinate at which to place the top-left corner of the image.", example = "0")
float x,
@RequestParam("y")
@Parameter(description = "The y-coordinate at which to place the top-left corner of the image.", example = "0")
float y,
@RequestParam("everyPage")
@Parameter(description = "Whether to overlay the image onto every page of the PDF.", example = "false")
boolean everyPage) {
public ResponseEntity<byte[]> overlayImage(@ModelAttribute OverlayImageRequest request) {
MultipartFile pdfFile = request.getFileInput();
MultipartFile imageFile = request.getImageFile();
float x = request.getX();
float y = request.getY();
boolean everyPage = request.isEveryPage();
try {
byte[] pdfBytes = pdfFile.getBytes();
byte[] imageBytes = imageFile.getBytes();

View File

@@ -0,0 +1,135 @@
package stirling.software.SPDF.controller.api.misc;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.List;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.font.PDType1Font;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.misc.AddPageNumbersRequest;
import stirling.software.SPDF.utils.GeneralUtils;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@RequestMapping("/api/v1/misc")
@Tag(name = "Misc", description = "Miscellaneous APIs")
public class PageNumbersController {
private static final Logger logger = LoggerFactory.getLogger(PageNumbersController.class);
@PostMapping(value = "/add-page-numbers", consumes = "multipart/form-data")
@Operation(summary = "Add page numbers to a PDF document", description = "This operation takes an input PDF file and adds page numbers to it. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> addPageNumbers(@ModelAttribute AddPageNumbersRequest request) throws IOException {
MultipartFile file = request.getFileInput();
String customMargin = request.getCustomMargin();
int position = request.getPosition();
int startingNumber = request.getStartingNumber();
String pagesToNumber = request.getPagesToNumber();
String customText = request.getCustomText();
int pageNumber = startingNumber;
byte[] fileBytes = file.getBytes();
PDDocument document = PDDocument.load(fileBytes);
float marginFactor;
switch (customMargin.toLowerCase()) {
case "small":
marginFactor = 0.02f;
break;
case "medium":
marginFactor = 0.035f;
break;
case "large":
marginFactor = 0.05f;
break;
case "x-large":
marginFactor = 0.075f;
break;
default:
marginFactor = 0.035f;
break;
}
float fontSize = 12.0f;
PDType1Font font = PDType1Font.HELVETICA;
if(pagesToNumber == null || pagesToNumber.length() == 0) {
pagesToNumber = "all";
}
if(customText == null || customText.length() == 0) {
customText = "{n}";
}
List<Integer> pagesToNumberList = GeneralUtils.parsePageList(pagesToNumber.split(","), document.getNumberOfPages());
for (int i : pagesToNumberList) {
PDPage page = document.getPage(i);
PDRectangle pageSize = page.getMediaBox();
String text = customText != null ? customText.replace("{n}", String.valueOf(pageNumber)).replace("{total}", String.valueOf(document.getNumberOfPages())).replace("{filename}", file.getOriginalFilename().replaceFirst("[.][^.]+$", "")) : String.valueOf(pageNumber);
float x, y;
int xGroup = (position - 1) % 3;
int yGroup = 2 - (position - 1) / 3;
switch (xGroup) {
case 0: // left
x = pageSize.getLowerLeftX() + marginFactor * pageSize.getWidth();
break;
case 1: // center
x = pageSize.getLowerLeftX() + (pageSize.getWidth() / 2);
break;
default: // right
x = pageSize.getUpperRightX() - marginFactor * pageSize.getWidth();
break;
}
switch (yGroup) {
case 0: // bottom
y = pageSize.getLowerLeftY() + marginFactor * pageSize.getHeight();
break;
case 1: // middle
y = pageSize.getLowerLeftY() + (pageSize.getHeight() / 2);
break;
default: // top
y = pageSize.getUpperRightY() - marginFactor * pageSize.getHeight();
break;
}
PDPageContentStream contentStream = new PDPageContentStream(document, page, PDPageContentStream.AppendMode.APPEND, true);
contentStream.beginText();
contentStream.setFont(font, fontSize);
contentStream.newLineAtOffset(x, y);
contentStream.showText(text);
contentStream.endText();
contentStream.close();
pageNumber++;
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
document.save(baos);
document.close();
return WebResponseUtils.bytesToWebResponse(baos.toByteArray(), file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_numbersAdded.pdf", MediaType.APPLICATION_PDF);
}
}

View File

@@ -1,4 +1,4 @@
package stirling.software.SPDF.controller.api.other;
package stirling.software.SPDF.controller.api.misc;
import java.io.IOException;
import java.nio.file.Files;
@@ -9,20 +9,22 @@ import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.RequestPart;
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.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.PDFFile;
import stirling.software.SPDF.utils.ProcessExecutor;
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@Tag(name = "Other", description = "Other APIs")
@RequestMapping("/api/v1/misc")
@Tag(name = "Misc", description = "Miscellaneous APIs")
public class RepairController {
private static final Logger logger = LoggerFactory.getLogger(RepairController.class);
@@ -32,11 +34,8 @@ public class RepairController {
summary = "Repair a PDF file",
description = "This endpoint repairs a given PDF file by running Ghostscript command. The PDF is first saved to a temporary location, repaired, read back, and then returned as a response. Input:PDF Output:PDF Type:SISO"
)
public ResponseEntity<byte[]> repairPdf(
@RequestPart(required = true, value = "fileInput")
@Parameter(description = "The input PDF file to be repaired", required = true)
MultipartFile inputFile) throws IOException, InterruptedException {
public ResponseEntity<byte[]> repairPdf(@ModelAttribute PDFFile request) throws IOException, InterruptedException {
MultipartFile inputFile = request.getFileInput();
// Save the uploaded file to a temporary location
Path tempInputFile = Files.createTempFile("input_", ".pdf");
inputFile.transferTo(tempInputFile.toFile());

View File

@@ -0,0 +1,63 @@
package stirling.software.SPDF.controller.api.misc;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.common.PDNameTreeNode;
import org.apache.pdfbox.pdmodel.interactive.action.PDActionJavaScript;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.PDFFile;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@RequestMapping("/api/v1/misc")
@Tag(name = "Misc", description = "Miscellaneous APIs")
public class ShowJavascript {
private static final Logger logger = LoggerFactory.getLogger(ShowJavascript.class);
@PostMapping(consumes = "multipart/form-data", value = "/show-javascript")
@Operation(summary = "Grabs all JS from a PDF and returns a single JS file with all code", description = "desc. Input:PDF Output:JS Type:SISO")
public ResponseEntity<byte[]> extractHeader(@ModelAttribute PDFFile request) throws Exception {
MultipartFile inputFile = request.getFileInput();
String script = "";
try (PDDocument document = PDDocument.load(inputFile.getInputStream())) {
if(document.getDocumentCatalog() != null && document.getDocumentCatalog().getNames() != null) {
PDNameTreeNode<PDActionJavaScript> jsTree = document.getDocumentCatalog().getNames().getJavaScript();
if (jsTree != null) {
Map<String, PDActionJavaScript> jsEntries = jsTree.getNames();
for (Map.Entry<String, PDActionJavaScript> entry : jsEntries.entrySet()) {
String name = entry.getKey();
PDActionJavaScript jsAction = entry.getValue();
String jsCodeStr = jsAction.getAction();
script += "// File: " + inputFile.getOriginalFilename() + ", Script: " + name + "\n" + jsCodeStr + "\n";
}
}
}
if (script.isEmpty()) {
script = "PDF '" + inputFile.getOriginalFilename() + "' does not contain Javascript";
}
return WebResponseUtils.bytesToWebResponse(script.getBytes(StandardCharsets.UTF_8), inputFile.getOriginalFilename() + ".js");
}
}
}

View File

@@ -1,174 +0,0 @@
package stirling.software.SPDF.controller.api.other;
import java.io.IOException;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
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.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.utils.GeneralUtils;
import stirling.software.SPDF.utils.PdfUtils;
import stirling.software.SPDF.utils.WebResponseUtils;
import org.apache.pdfbox.pdmodel.*;
import org.apache.pdfbox.pdmodel.common.*;
import org.apache.pdfbox.pdmodel.PDPageContentStream.*;
import org.springframework.web.bind.annotation.*;
import org.springframework.http.*;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.*;
import io.swagger.v3.oas.annotations.media.*;
import io.swagger.v3.oas.annotations.parameters.*;
import org.apache.pdfbox.pdmodel.font.PDType1Font;
import org.apache.tomcat.util.http.ResponseUtil;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.URLEncoder;
import java.util.List;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
import com.itextpdf.io.font.constants.StandardFonts;
import com.itextpdf.kernel.font.PdfFont;
import com.itextpdf.kernel.font.PdfFontFactory;
import com.itextpdf.kernel.geom.Rectangle;
import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfPage;
import com.itextpdf.kernel.pdf.canvas.PdfCanvas;
import com.itextpdf.layout.Canvas;
import com.itextpdf.layout.element.Paragraph;
import com.itextpdf.layout.properties.TextAlignment;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.*;
@RestController
@Tag(name = "Other", description = "Other APIs")
public class PageNumbersController {
private static final Logger logger = LoggerFactory.getLogger(PageNumbersController.class);
@PostMapping(value = "/add-page-numbers", consumes = "multipart/form-data")
@Operation(summary = "Add page numbers to a PDF document", description = "This operation takes an input PDF file and adds page numbers to it. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> addPageNumbers(
@Parameter(description = "The input PDF file", required = true) @RequestParam("fileInput") MultipartFile file,
@Parameter(description = "Custom margin: small/medium/large", required = true, schema = @Schema(type = "string", allowableValues = {"small", "medium", "large"})) @RequestParam("customMargin") String customMargin,
@Parameter(description = "Position: 1 of 9 positions", required = true, schema = @Schema(type = "integer", minimum = "1", maximum = "9")) @RequestParam("position") int position,
@Parameter(description = "Starting number", required = true, schema = @Schema(type = "integer", minimum = "1")) @RequestParam("startingNumber") int startingNumber,
@Parameter(description = "Which pages to number, default all", required = false, schema = @Schema(type = "string")) @RequestParam(value = "pagesToNumber", required = false) String pagesToNumber,
@Parameter(description = "Custom text: defaults to just number but can have things like \"Page {n} of {p}\"", required = false, schema = @Schema(type = "string")) @RequestParam(value = "customText", required = false) String customText)
throws IOException {
byte[] fileBytes = file.getBytes();
ByteArrayInputStream bais = new ByteArrayInputStream(fileBytes);
int pageNumber = startingNumber;
float marginFactor;
switch (customMargin.toLowerCase()) {
case "small":
marginFactor = 0.02f;
break;
case "medium":
marginFactor = 0.035f;
break;
case "large":
marginFactor = 0.05f;
break;
case "x-large":
marginFactor = 0.1f;
break;
default:
marginFactor = 0.035f;
break;
}
float fontSize = 12.0f;
PdfReader reader = new PdfReader(bais);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
PdfWriter writer = new PdfWriter(baos);
PdfDocument pdfDoc = new PdfDocument(reader, writer);
List<Integer> pagesToNumberList = GeneralUtils.parsePageList(pagesToNumber.split(","), pdfDoc.getNumberOfPages());
for (int i : pagesToNumberList) {
PdfPage page = pdfDoc.getPage(i+1);
Rectangle pageSize = page.getPageSize();
PdfCanvas pdfCanvas = new PdfCanvas(page.newContentStreamAfter(), page.getResources(), pdfDoc);
String text = customText != null ? customText.replace("{n}", String.valueOf(pageNumber)).replace("{total}", String.valueOf(pdfDoc.getNumberOfPages())) : String.valueOf(pageNumber);
PdfFont font = PdfFontFactory.createFont(StandardFonts.HELVETICA);
float textWidth = font.getWidth(text, fontSize);
float textHeight = font.getAscent(text, fontSize) - font.getDescent(text, fontSize);
float x, y;
TextAlignment alignment;
int xGroup = (position - 1) % 3;
int yGroup = 2 - (position - 1) / 3;
switch (xGroup) {
case 0: // left
x = pageSize.getLeft() + marginFactor * pageSize.getWidth();
alignment = TextAlignment.LEFT;
break;
case 1: // center
x = pageSize.getLeft() + (pageSize.getWidth()) / 2;
alignment = TextAlignment.CENTER;
break;
default: // right
x = pageSize.getRight() - marginFactor * pageSize.getWidth();
alignment = TextAlignment.RIGHT;
break;
}
switch (yGroup) {
case 0: // bottom
y = pageSize.getBottom() + marginFactor * pageSize.getHeight();
break;
case 1: // middle
y = pageSize.getBottom() + (pageSize.getHeight() ) / 2;
break;
default: // top
y = pageSize.getTop() - marginFactor * pageSize.getHeight();
break;
}
new Canvas(pdfCanvas, page.getPageSize())
.showTextAligned(new Paragraph(text).setFont(font).setFontSize(fontSize), x, y, alignment);
pageNumber++;
}
pdfDoc.close();
byte[] resultBytes = baos.toByteArray();
return WebResponseUtils.bytesToWebResponse(resultBytes, URLEncoder.encode(file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_numbersAdded.pdf", "UTF-8"), MediaType.APPLICATION_PDF);
}
}

View File

@@ -1,141 +0,0 @@
package stirling.software.SPDF.controller.api.other;
import java.io.IOException;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
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.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.utils.GeneralUtils;
import stirling.software.SPDF.utils.PdfUtils;
import stirling.software.SPDF.utils.WebResponseUtils;
import org.apache.pdfbox.pdmodel.*;
import org.apache.pdfbox.pdmodel.common.*;
import org.apache.pdfbox.pdmodel.PDPageContentStream.*;
import org.springframework.web.bind.annotation.*;
import org.springframework.http.*;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.*;
import io.swagger.v3.oas.annotations.media.*;
import io.swagger.v3.oas.annotations.parameters.*;
import org.apache.pdfbox.pdmodel.font.PDType1Font;
import org.apache.pdfbox.text.TextPosition;
import org.apache.tomcat.util.http.ResponseUtil;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.ArrayList;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.itextpdf.io.font.constants.StandardFonts;
import com.itextpdf.kernel.font.PdfFont;
import com.itextpdf.kernel.font.PdfFontFactory;
import com.itextpdf.kernel.geom.Rectangle;
import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.kernel.pdf.PdfStream;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.kernel.pdf.PdfArray;
import com.itextpdf.kernel.pdf.PdfDictionary;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfName;
import com.itextpdf.kernel.pdf.PdfObject;
import com.itextpdf.kernel.pdf.PdfPage;
import com.itextpdf.kernel.pdf.canvas.PdfCanvas;
import com.itextpdf.layout.Canvas;
import com.itextpdf.layout.element.Paragraph;
import com.itextpdf.layout.properties.TextAlignment;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.*;
import org.apache.pdfbox.pdmodel.*;
import org.apache.pdfbox.text.*;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.*;
import io.swagger.v3.oas.annotations.media.Schema;
import org.springframework.http.ResponseEntity;
@RestController
@Tag(name = "Other", description = "Other APIs")
public class ShowJavascript {
private static final Logger logger = LoggerFactory.getLogger(ShowJavascript.class);
@PostMapping(consumes = "multipart/form-data", value = "/show-javascript")
@Operation(summary = "Extract header from PDF file", description = "This endpoint accepts a PDF file and attempts to extract its title or header based on heuristics. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> extractHeader(
@RequestPart(value = "fileInput") @Parameter(description = "The input PDF file from which the javascript is to be extracted.", required = true) MultipartFile inputFile)
throws Exception {
try (
PdfDocument itextDoc = new PdfDocument(new PdfReader(inputFile.getInputStream()))
) {
String name = "";
String script = "";
String entryName = "File: "+inputFile.getOriginalFilename() + ", Script: ";
//Javascript
PdfDictionary namesDict = itextDoc.getCatalog().getPdfObject().getAsDictionary(PdfName.Names);
if (namesDict != null) {
PdfDictionary javascriptDict = namesDict.getAsDictionary(PdfName.JavaScript);
if (javascriptDict != null) {
PdfArray namesArray = javascriptDict.getAsArray(PdfName.Names);
for (int i = 0; i < namesArray.size(); i += 2) {
if(namesArray.getAsString(i) != null)
name = namesArray.getAsString(i).toString();
PdfObject jsCode = namesArray.get(i+1);
if (jsCode instanceof PdfStream) {
byte[] jsCodeBytes = ((PdfStream)jsCode).getBytes();
String jsCodeStr = new String(jsCodeBytes, StandardCharsets.UTF_8);
script = "//" + entryName + name + "\n" +jsCodeStr;
} else if (jsCode instanceof PdfDictionary) {
// If the JS code is in a dictionary, you'll need to know the key to use.
// Assuming the key is PdfName.JS:
PdfStream jsCodeStream = ((PdfDictionary)jsCode).getAsStream(PdfName.JS);
if (jsCodeStream != null) {
byte[] jsCodeBytes = jsCodeStream.getBytes();
String jsCodeStr = new String(jsCodeBytes, StandardCharsets.UTF_8);
script = "//" + entryName + name + "\n" +jsCodeStr;
}
}
}
}
}
if(script.equals("")) {
script = "PDF '" +inputFile.getOriginalFilename() + "' does not contain Javascript";
}
return WebResponseUtils.bytesToWebResponse(script.getBytes(), name + ".js");
}
}
}

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