Compare commits
638 Commits
v0.14.2
...
normalize_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd07f789ce | ||
|
|
d725cf9a01 | ||
|
|
3dd0471e22 | ||
|
|
5a4efa81a7 | ||
|
|
ea59c12b27 | ||
|
|
9600f91dda | ||
|
|
a9f93b014a | ||
|
|
bf62e389f7 | ||
|
|
26af6b5636 | ||
|
|
0fabfea56d | ||
|
|
7a9417a62f | ||
|
|
eb45005baa | ||
|
|
a4f923eb3a | ||
|
|
e1c3561997 | ||
|
|
bf8b902100 | ||
|
|
8a5883501a | ||
|
|
fd8f3ce019 | ||
|
|
6f72096953 | ||
|
|
5a52e3d6dd | ||
|
|
23672cd18d | ||
|
|
68c0941666 | ||
|
|
96e399a617 | ||
|
|
22343e507d | ||
|
|
8a143d139c | ||
|
|
15ad46fe1c | ||
|
|
2473f0d034 | ||
|
|
f211eefc85 | ||
|
|
9da88b7652 | ||
|
|
729c8006d2 | ||
|
|
0d5b790443 | ||
|
|
aa16035137 | ||
|
|
41686883ee | ||
|
|
2e0790c893 | ||
|
|
4e937a6024 | ||
|
|
4af58118c9 | ||
|
|
aa2ad33c1d | ||
|
|
c7005bc07f | ||
|
|
3f932ebec9 | ||
|
|
296f265391 | ||
|
|
ca8519cb10 | ||
|
|
734d76a3b5 | ||
|
|
f5a39ed514 | ||
|
|
96f4e5eac7 | ||
|
|
48be772703 | ||
|
|
a9edb49723 | ||
|
|
95471a2fba | ||
|
|
36c277961f | ||
|
|
734fff5618 | ||
|
|
16136b2f6f | ||
|
|
c8dfe10a7c | ||
|
|
c8481fdbef | ||
|
|
8e0c02a151 | ||
|
|
271906097d | ||
|
|
91caa2a097 | ||
|
|
6105451e08 | ||
|
|
450e090252 | ||
|
|
86635f85b4 | ||
|
|
a7214a2171 | ||
|
|
61cd473e6c | ||
|
|
d67690d995 | ||
|
|
e20f4fe31a | ||
|
|
2deb40bb6d | ||
|
|
bfee745cca | ||
|
|
68d390e633 | ||
|
|
a884f1b3d4 | ||
|
|
d190ae0cf3 | ||
|
|
bb08a63296 | ||
|
|
3debc1b0df | ||
|
|
e560028097 | ||
|
|
5afcbdbc8b | ||
|
|
aaaf3ffe34 | ||
|
|
b043e666ae | ||
|
|
cda8f7b27d | ||
|
|
d524fcc157 | ||
|
|
e05e34f217 | ||
|
|
73bbb516d2 | ||
|
|
c2aaa65228 | ||
|
|
91722af8b0 | ||
|
|
71d33f6047 | ||
|
|
903faadff3 | ||
|
|
55020d45f8 | ||
|
|
2d37c707e2 | ||
|
|
b00f8c80ec | ||
|
|
53afb865c5 | ||
|
|
6f3e317484 | ||
|
|
e77d2847ea | ||
|
|
46abae9acc | ||
|
|
571320b9ba | ||
|
|
07e9fcb6cc | ||
|
|
7a8719743d | ||
|
|
746f2d0949 | ||
|
|
3986858adb | ||
|
|
589cb8d91f | ||
|
|
705c75e51d | ||
|
|
6acb593411 | ||
|
|
8060451713 | ||
|
|
6130f14d5a | ||
|
|
0fbc461877 | ||
|
|
89e461e4f6 | ||
|
|
ba4ad1aff9 | ||
|
|
be1904749b | ||
|
|
166fa0eb87 | ||
|
|
9a06e7a3ca | ||
|
|
cb12af2d95 | ||
|
|
46032b8ebb | ||
|
|
4a6bd60466 | ||
|
|
f85c8ea5ec | ||
|
|
06ef09035d | ||
|
|
13301e4606 | ||
|
|
b59651a0fb | ||
|
|
86b8d7f804 | ||
|
|
e4dded3faa | ||
|
|
75cf3ed0c1 | ||
|
|
2fa68be36b | ||
|
|
d09b252a4a | ||
|
|
a5165b04cd | ||
|
|
1bd17eded6 | ||
|
|
18d289d3b7 | ||
|
|
e43e6d18b9 | ||
|
|
ec5a3c5948 | ||
|
|
89c0e721b8 | ||
|
|
c807d20590 | ||
|
|
686af16cf5 | ||
|
|
219dd7834f | ||
|
|
1b83fda349 | ||
|
|
490acddc65 | ||
|
|
e69ed06b4f | ||
|
|
2fe9b5a24b | ||
|
|
3912f42128 | ||
|
|
801e307005 | ||
|
|
c8acddb251 | ||
|
|
d8cf7e81b9 | ||
|
|
c4ad442ec3 | ||
|
|
c8e5023ec1 | ||
|
|
5281d7a49a | ||
|
|
77dcf04cfe | ||
|
|
b6523e9989 | ||
|
|
d52a00185b | ||
|
|
00487275a7 | ||
|
|
823c8eb53e | ||
|
|
e9b8981a35 | ||
|
|
575e0b3e54 | ||
|
|
db931717a1 | ||
|
|
787c59efd3 | ||
|
|
45aead89e3 | ||
|
|
81d49b722b | ||
|
|
ab9e7bbb8c | ||
|
|
ee223d0405 | ||
|
|
99050ad73e | ||
|
|
9fc873e973 | ||
|
|
52fe4c6aa6 | ||
|
|
66df7053bb | ||
|
|
edde1a6436 | ||
|
|
50ee829e5f | ||
|
|
1924dfb4f1 | ||
|
|
873a4ecb7e | ||
|
|
32da14acbf | ||
|
|
139c793b5e | ||
|
|
e717d83f75 | ||
|
|
ef12c2f892 | ||
|
|
362a7ff434 | ||
|
|
624e015315 | ||
|
|
d76752d7f6 | ||
|
|
7260e578e3 | ||
|
|
2544b762ba | ||
|
|
164d1abdbb | ||
|
|
863b48b5a9 | ||
|
|
b5e0e147ac | ||
|
|
2fe454ea47 | ||
|
|
c8458ffe50 | ||
|
|
572f9f728f | ||
|
|
6c963e1b6c | ||
|
|
770b61bb7a | ||
|
|
db64b3f71d | ||
|
|
5fad085db5 | ||
|
|
7ed8a69326 | ||
|
|
5d1786cda0 | ||
|
|
550f8b0eea | ||
|
|
b5d5b6e3e2 | ||
|
|
97b6f0eeb4 | ||
|
|
bd7e2fea0b | ||
|
|
eee7e4d707 | ||
|
|
43410de851 | ||
|
|
37c92ee9aa | ||
|
|
351cf25f86 | ||
|
|
10cb02020c | ||
|
|
5e40f00bae | ||
|
|
cebc0daf2b | ||
|
|
04f3f735fc | ||
|
|
f7ef8c32aa | ||
|
|
49f2071a93 | ||
|
|
ecb62e0c94 | ||
|
|
846ebe6dda | ||
|
|
56afd35c82 | ||
|
|
eadd513b02 | ||
|
|
97f581ad6d | ||
|
|
d96a3db60a | ||
|
|
0592bac5bf | ||
|
|
4b0df4ffd5 | ||
|
|
a244d563f2 | ||
|
|
f433e8032f | ||
|
|
23b85dc47c | ||
|
|
6a9ef7d538 | ||
|
|
c75efede79 | ||
|
|
a0212bbfb7 | ||
|
|
87efa175cb | ||
|
|
ad7150d616 | ||
|
|
6fe268adcb | ||
|
|
0c2b05eabf | ||
|
|
38ebc28108 | ||
|
|
0a08831aac | ||
|
|
2a744473f9 | ||
|
|
56ce53a966 | ||
|
|
6baf1f94c1 | ||
|
|
8a57165547 | ||
|
|
de9e9a0f84 | ||
|
|
73a55c0666 | ||
|
|
4ac5262be2 | ||
|
|
dfee149da0 | ||
|
|
fbe0a8ddcc | ||
|
|
56a1867270 | ||
|
|
c23a5ad5fb | ||
|
|
31fbeaae1d | ||
|
|
e0d79990c8 | ||
|
|
468808167c | ||
|
|
5af5794dfe | ||
|
|
1d470691a5 | ||
|
|
f32832f70d | ||
|
|
cd0464092a | ||
|
|
c67eaf2b4d | ||
|
|
b1f80bc9f6 | ||
|
|
f3742ebeb6 | ||
|
|
adc7b9606b | ||
|
|
aa34257080 | ||
|
|
0f126eaf81 | ||
|
|
7389543af6 | ||
|
|
9795c68220 | ||
|
|
7ffa447cbc | ||
|
|
d755fd1861 | ||
|
|
e273294360 | ||
|
|
827ed62761 | ||
|
|
ee96d2a0e3 | ||
|
|
044a779a7c | ||
|
|
03a8f45128 | ||
|
|
b5423f3434 | ||
|
|
88c993367f | ||
|
|
04acdb3b02 | ||
|
|
cd3cc15888 | ||
|
|
76e6a23674 | ||
|
|
4fbfd0bae4 | ||
|
|
b74819cf6c | ||
|
|
a5ad9e13fe | ||
|
|
328e873344 | ||
|
|
39045df785 | ||
|
|
d83bd1ae94 | ||
|
|
143b770882 | ||
|
|
d91c600925 | ||
|
|
cbac784c57 | ||
|
|
f535387ac4 | ||
|
|
739dcc1327 | ||
|
|
3864e130cc | ||
|
|
5b0145fa47 | ||
|
|
7dfeb4bb0f | ||
|
|
eda91cc556 | ||
|
|
7572db9bd4 | ||
|
|
9e3b50dff3 | ||
|
|
cf640c7e3f | ||
|
|
ec770e1008 | ||
|
|
15e0048bfc | ||
|
|
b572a5e4c9 | ||
|
|
c55a5657a4 | ||
|
|
5f771b7851 | ||
|
|
c853465d1d | ||
|
|
b58cbdcb61 | ||
|
|
9e81667ecd | ||
|
|
a92479b505 | ||
|
|
6ca9001fe6 | ||
|
|
279cfa70f5 | ||
|
|
f8ad71aa4e | ||
|
|
524d198212 | ||
|
|
1baf458344 | ||
|
|
da50e4d212 | ||
|
|
bbd8de0899 | ||
|
|
c548aa037e | ||
|
|
6a7ed615e3 | ||
|
|
56cbb4381b | ||
|
|
4612b05199 | ||
|
|
7b43fca6fc | ||
|
|
e3c8af7e54 | ||
|
|
63eacf443e | ||
|
|
b32c28e9cb | ||
|
|
a5ee10e029 | ||
|
|
bb1d41d74a | ||
|
|
0698e2888d | ||
|
|
e1f0a6cb1d | ||
|
|
7fecae8b0d | ||
|
|
e6622dfdc4 | ||
|
|
34b4ae0e03 | ||
|
|
036fd711f9 | ||
|
|
80a59205fa | ||
|
|
cbe4bca716 | ||
|
|
232a556425 | ||
|
|
a497ad8c41 | ||
|
|
3041e80c37 | ||
|
|
1b2df20fdd | ||
|
|
0e69f7e0e8 | ||
|
|
94aba370e0 | ||
|
|
cd49d7ffa2 | ||
|
|
dc297644d1 | ||
|
|
a7b4e44e6d | ||
|
|
610ff22abe | ||
|
|
27e8335f79 | ||
|
|
168a0f001c | ||
|
|
5c6936b494 | ||
|
|
a715dbb25d | ||
|
|
a43e13cf94 | ||
|
|
7f805d16a1 | ||
|
|
6f3cbe0cae | ||
|
|
44e3556382 | ||
|
|
c5ba546a02 | ||
|
|
d63519bbf4 | ||
|
|
aeadc88f92 | ||
|
|
829e98c29b | ||
|
|
3e3f4a0188 | ||
|
|
e5bdd52b7c | ||
|
|
e653ef6522 | ||
|
|
5fcb4e893b | ||
|
|
c2b524e459 | ||
|
|
5a055bae5e | ||
|
|
c3f501d701 | ||
|
|
8acab77ae3 | ||
|
|
8bd2784f37 | ||
|
|
e2c5027311 | ||
|
|
d2b2adcbc1 | ||
|
|
48158379ee | ||
|
|
6ba84a190f | ||
|
|
d349aea1be | ||
|
|
79e2683cbe | ||
|
|
e4fb64ce16 | ||
|
|
d405b7a810 | ||
|
|
1d243a0ca5 | ||
|
|
1f10693eaf | ||
|
|
b7f62a635d | ||
|
|
4e991e7ec2 | ||
|
|
8fe7e57a6a | ||
|
|
5c79a5da29 | ||
|
|
d01473aceb | ||
|
|
3911be0177 | ||
|
|
78da44ad83 | ||
|
|
54859ac3ba | ||
|
|
9cb8c9f655 | ||
|
|
dfda474ba5 | ||
|
|
43f15b3e55 | ||
|
|
86c45f6f8f | ||
|
|
de83321c62 | ||
|
|
7b44cf77d6 | ||
|
|
c769a02982 | ||
|
|
aa671b8bd6 | ||
|
|
6e7c066e57 | ||
|
|
78ac9231c5 | ||
|
|
e9947da5b4 | ||
|
|
8df7dfc3be | ||
|
|
d79db6f3da | ||
|
|
84aebe3851 | ||
|
|
f5c285a70f | ||
|
|
2d6bf43bdb | ||
|
|
964f22e3e0 | ||
|
|
d325020e22 | ||
|
|
aec85ddd66 | ||
|
|
32b009b11f | ||
|
|
2e5b72e4fb | ||
|
|
1d3cf2bdc3 | ||
|
|
2a5fe2bd74 | ||
|
|
61ff0248da | ||
|
|
659af2089c | ||
|
|
8960313a2b | ||
|
|
6ee8e1e37f | ||
|
|
05977aa3a6 | ||
|
|
f7dbb8d0a6 | ||
|
|
eaf65d7981 | ||
|
|
a10e3a025b | ||
|
|
4d3e442ecc | ||
|
|
49576c0aa4 | ||
|
|
960af83f11 | ||
|
|
cf42ef7faa | ||
|
|
d894937c22 | ||
|
|
8938e86223 | ||
|
|
c1a39e53dc | ||
|
|
cf3693186a | ||
|
|
0fb0cb8bca | ||
|
|
fb18d0d04d | ||
|
|
779d9028fe | ||
|
|
f2b701e3e3 | ||
|
|
a138d5f5a9 | ||
|
|
6276f028ac | ||
|
|
b962e867d8 | ||
|
|
a286a92ede | ||
|
|
7fb8f5ed28 | ||
|
|
03d3235e1d | ||
|
|
d23551857c | ||
|
|
dd9dd72f35 | ||
|
|
9652f59ae9 | ||
|
|
3469beb5b3 | ||
|
|
690720f4e3 | ||
|
|
491be75e1f | ||
|
|
a868b2c649 | ||
|
|
0b49993d80 | ||
|
|
995a926e35 | ||
|
|
914dd0a21a | ||
|
|
d9b5d08b06 | ||
|
|
344d1163ff | ||
|
|
3f50979d3e | ||
|
|
c681f48459 | ||
|
|
2f5d7ed712 | ||
|
|
1efefcfcb8 | ||
|
|
909c9ed4d9 | ||
|
|
116b3535ee | ||
|
|
b7d6107a2d | ||
|
|
120b017b1a | ||
|
|
24568f4a42 | ||
|
|
03450454c5 | ||
|
|
7e982e125d | ||
|
|
e725451530 | ||
|
|
d7d6bc8108 | ||
|
|
6d66ac0a8b | ||
|
|
93f12d1313 | ||
|
|
eab9e3cffc | ||
|
|
d74c25e678 | ||
|
|
c729b7201f | ||
|
|
beab9932d7 | ||
|
|
65fcf29fd5 | ||
|
|
73007239ee | ||
|
|
816d874ac4 | ||
|
|
b66f86f7cc | ||
|
|
168ef747de | ||
|
|
ad047ab012 | ||
|
|
1ea3fb209b | ||
|
|
875d9da36b | ||
|
|
b21d2ecbd1 | ||
|
|
3bffc1da76 | ||
|
|
631d3948bd | ||
|
|
5774a22b64 | ||
|
|
39345bb6bb | ||
|
|
57b483047e | ||
|
|
0fb7633da8 | ||
|
|
dae2f33772 | ||
|
|
31ac877612 | ||
|
|
79dcf99cce | ||
|
|
c28a40ffe8 | ||
|
|
12dccab460 | ||
|
|
0a26e2e6d6 | ||
|
|
74f6cd63f4 | ||
|
|
e8de5739fa | ||
|
|
1b2734d99c | ||
|
|
206cf40cb5 | ||
|
|
78473e96fd | ||
|
|
b7d6ac2cc3 | ||
|
|
4068d9530f | ||
|
|
8a331956c2 | ||
|
|
1d3e018a56 | ||
|
|
3602034938 | ||
|
|
eb4e2d5fca | ||
|
|
b6671939e5 | ||
|
|
41d09e40a1 | ||
|
|
9b0dba7f65 | ||
|
|
ac0dc8b5c7 | ||
|
|
723216c693 | ||
|
|
46f9a5057f | ||
|
|
8a2633ca93 | ||
|
|
a03470d2de | ||
|
|
ef7c98e5cb | ||
|
|
c9cd1331d2 | ||
|
|
ddc14517b8 | ||
|
|
578aecf977 | ||
|
|
b1ca938053 | ||
|
|
298fe349c1 | ||
|
|
f4364a3f33 | ||
|
|
e0f068bc9d | ||
|
|
d7f8219b80 | ||
|
|
87bc0fc975 | ||
|
|
9f21ce96de | ||
|
|
1f29033f17 | ||
|
|
59c7978330 | ||
|
|
8b55ffff96 | ||
|
|
a94808fd19 | ||
|
|
7b2ffcff01 | ||
|
|
28a9daff62 | ||
|
|
435753f50b | ||
|
|
1e6f288d72 | ||
|
|
6d3fece5a6 | ||
|
|
db926c50d8 | ||
|
|
dd9333f42e | ||
|
|
3fa5acc51c | ||
|
|
15fa3df424 | ||
|
|
06c4ec95d5 | ||
|
|
1a6afc1582 | ||
|
|
ad2e1e4a18 | ||
|
|
a1d0dcff41 | ||
|
|
08da0f5c56 | ||
|
|
0b666674f7 | ||
|
|
d3fe467f6f | ||
|
|
732fa0ec40 | ||
|
|
7a7c978df2 | ||
|
|
9d052b310f | ||
|
|
8ff1a63276 | ||
|
|
ffd413ce7f | ||
|
|
f2607bd161 | ||
|
|
ba5f3e12d7 | ||
|
|
ddc48429b1 | ||
|
|
b8b7adbaf9 | ||
|
|
4ae945d08a | ||
|
|
12f5a5e6d0 | ||
|
|
f85a7cb04d | ||
|
|
2f6a885bb0 | ||
|
|
c8ac1f7029 | ||
|
|
d6afb07533 | ||
|
|
a55f9f0ec8 | ||
|
|
06401d875b | ||
|
|
4a29fd4b73 | ||
|
|
02c53b90b3 | ||
|
|
6ca1d82188 | ||
|
|
18c5f5bb2b | ||
|
|
33d21a7a85 | ||
|
|
c48c3e8897 | ||
|
|
67f34016ce | ||
|
|
c1434df259 | ||
|
|
dd0eaf9182 | ||
|
|
cfe50bcd81 | ||
|
|
a75bbff7cf | ||
|
|
2e9d88da0e | ||
|
|
124c7801c5 | ||
|
|
8490613ada | ||
|
|
80553ce95a | ||
|
|
d9206bfd2a | ||
|
|
7aae688db2 | ||
|
|
347b4cfa85 | ||
|
|
f2eebcc396 | ||
|
|
19c26f0552 | ||
|
|
d532db91f9 | ||
|
|
bd0bf404f5 | ||
|
|
e51a9c209a | ||
|
|
6bf172fb25 | ||
|
|
a1e93e0f5d | ||
|
|
6392f6ec12 | ||
|
|
fbdff5c97f | ||
|
|
2ecc4ed080 | ||
|
|
3318cb96b2 | ||
|
|
6be0a1fb05 | ||
|
|
ab1297aee0 | ||
|
|
25a0cb7681 | ||
|
|
116b034878 | ||
|
|
038de2e264 | ||
|
|
7e51cf8c5a | ||
|
|
a1eadba769 | ||
|
|
3145f5fdd0 | ||
|
|
8393dd4731 | ||
|
|
768877d969 | ||
|
|
14a90f5e50 | ||
|
|
99b0150e7a | ||
|
|
3be12c8988 | ||
|
|
89345c8d60 | ||
|
|
49f1f4e7c7 | ||
|
|
d0ce7db9ee | ||
|
|
53a0291cc2 | ||
|
|
9a3bc839dd | ||
|
|
e519840bd6 | ||
|
|
ed32a3ca33 | ||
|
|
369ac99a16 | ||
|
|
39ac823b05 | ||
|
|
548ae4dba3 | ||
|
|
f69d593649 | ||
|
|
423af5f077 | ||
|
|
76e5e1ad00 | ||
|
|
420caa4d8d | ||
|
|
fdeeb68a6d | ||
|
|
9fe803a0b1 | ||
|
|
dd9ba90a03 | ||
|
|
2bc739316a | ||
|
|
74da8c340d | ||
|
|
766cb4410b | ||
|
|
78fe6d6ea8 | ||
|
|
6b99decb56 | ||
|
|
323745e61f | ||
|
|
22c19670e9 | ||
|
|
a1b7aaddb8 | ||
|
|
cfba9681c4 | ||
|
|
a019b8b5ca | ||
|
|
bd9b267562 | ||
|
|
b0f8f56650 | ||
|
|
a71e813e82 | ||
|
|
75e7665d6e | ||
|
|
12293f5297 | ||
|
|
4899ee0bee | ||
|
|
563c17c84e | ||
|
|
d94eca4ee7 | ||
|
|
f4a01884bd | ||
|
|
105d7f12ac | ||
|
|
30c115a7de | ||
|
|
88a90f22a3 | ||
|
|
c72c712c1b | ||
|
|
7205801e76 | ||
|
|
64f4f54b9d | ||
|
|
0287d88895 | ||
|
|
cdc075b27c | ||
|
|
604d9827c5 | ||
|
|
bdeb6bf188 | ||
|
|
19e122be99 | ||
|
|
f3ddf18a23 | ||
|
|
db488b39bb | ||
|
|
51f863e1e4 | ||
|
|
0a9381d538 | ||
|
|
a9514b54eb | ||
|
|
d647cb196f | ||
|
|
b69973f614 | ||
|
|
fb9c42f4a1 | ||
|
|
b873e3cdf8 | ||
|
|
e9fc024332 | ||
|
|
d4956fad8c | ||
|
|
9d1dfe742e | ||
|
|
e32c092af8 | ||
|
|
18a2664b54 | ||
|
|
954e46c5ec | ||
|
|
e0f306d3f7 | ||
|
|
09db6618d6 | ||
|
|
c5ea254945 | ||
|
|
1fc1ecbaa6 | ||
|
|
bc4640c3f0 | ||
|
|
1e2eb9b07a | ||
|
|
ece00956d9 | ||
|
|
af5bbd8838 | ||
|
|
3a8f2495ea | ||
|
|
993f5e5097 | ||
|
|
1f99c26e78 | ||
|
|
42907ade21 | ||
|
|
b3bc0b4e5a | ||
|
|
12e24f3ec1 | ||
|
|
42610b2645 | ||
|
|
4e06e8c0c0 |
2
.git-blame-ignore-revs
Normal file
2
.git-blame-ignore-revs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# Formatting
|
||||||
|
5f771b785130154ed47952635b7acef371ffe0ec
|
||||||
3
.gitattributes
vendored
3
.gitattributes
vendored
@@ -1,5 +1,8 @@
|
|||||||
|
* text=auto eol=lf
|
||||||
|
|
||||||
# Ignore all JavaScript files in a directory
|
# 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/pdfjs/** linguist-vendored
|
||||||
src/main/resources/static/css/bootstrap-icons.css 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/bootstrap.min.css linguist-vendored
|
||||||
src/main/resources/static/css/fonts/* linguist-vendored
|
src/main/resources/static/css/fonts/* linguist-vendored
|
||||||
|
|||||||
2
.github/CODEOWNERS
vendored
Normal file
2
.github/CODEOWNERS
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# All PRs to V1 must be approved by Frooodle
|
||||||
|
* @Frooodle
|
||||||
4
.github/dependabot.yml
vendored
4
.github/dependabot.yml
vendored
@@ -9,3 +9,7 @@ updates:
|
|||||||
directory: "/" # Location of package manifests
|
directory: "/" # Location of package manifests
|
||||||
schedule:
|
schedule:
|
||||||
interval: "weekly"
|
interval: "weekly"
|
||||||
|
- package-ecosystem: "docker"
|
||||||
|
directory: "/" # Location of Dockerfile
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
|||||||
18
.github/pull_request_template.md
vendored
Normal file
18
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Description
|
||||||
|
|
||||||
|
Please provide a summary of the changes, including relevant motivation and context.
|
||||||
|
|
||||||
|
Closes #(issue_number)
|
||||||
|
|
||||||
|
## Checklist:
|
||||||
|
|
||||||
|
- [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
|
||||||
|
- [ ] I have performed a self-review of my own code
|
||||||
|
- [ ] I have commented my code, particularly in hard-to-understand areas
|
||||||
|
- [ ] My changes generate no new warnings
|
||||||
|
|
||||||
|
## Contributor License Agreement
|
||||||
|
|
||||||
|
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 the 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)
|
||||||
34
.github/workflows/build.yml
vendored
Normal file
34
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
name: "Build repo"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "main" ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ "main" ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
actions: read
|
||||||
|
contents: read
|
||||||
|
security-events: write
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set up JDK 17
|
||||||
|
uses: actions/setup-java@v3
|
||||||
|
with:
|
||||||
|
java-version: '17'
|
||||||
|
distribution: 'temurin'
|
||||||
|
|
||||||
|
- uses: gradle/gradle-build-action@v2.4.2
|
||||||
|
with:
|
||||||
|
gradle-version: 7.6
|
||||||
|
arguments: build --no-build-cache
|
||||||
55
.github/workflows/codeql.yml
vendored
55
.github/workflows/codeql.yml
vendored
@@ -1,55 +0,0 @@
|
|||||||
# For most projects, this workflow file will not need changing; you simply need
|
|
||||||
# to commit it to your repository.
|
|
||||||
#
|
|
||||||
# You may wish to alter this file to override the set of languages analyzed,
|
|
||||||
# or to provide custom queries or build logic.
|
|
||||||
#
|
|
||||||
# ******** NOTE ********
|
|
||||||
# We have attempted to detect the languages in your repository. Please check
|
|
||||||
# the `language` matrix defined below to confirm you have the correct set of
|
|
||||||
|
|
||||||
name: "Build repo"
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ "main" ]
|
|
||||||
pull_request:
|
|
||||||
# The branches below must be a subset of the branches above
|
|
||||||
branches: [ "main" ]
|
|
||||||
schedule:
|
|
||||||
- cron: '15 12 * * 1'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
analyze:
|
|
||||||
name: Analyze
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
actions: read
|
|
||||||
contents: read
|
|
||||||
security-events: write
|
|
||||||
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Set up JDK 17
|
|
||||||
uses: actions/setup-java@v3
|
|
||||||
with:
|
|
||||||
java-version: '17'
|
|
||||||
distribution: 'temurin'
|
|
||||||
|
|
||||||
# - name: Initialize CodeQL
|
|
||||||
# uses: github/codeql-action/init@v2
|
|
||||||
# with:
|
|
||||||
# languages: java
|
|
||||||
|
|
||||||
- uses: gradle/gradle-build-action@v2.4.2
|
|
||||||
with:
|
|
||||||
gradle-version: 7.6
|
|
||||||
arguments: assemble --no-build-cache
|
|
||||||
|
|
||||||
#- name: Perform CodeQL analysis
|
|
||||||
# uses: github/codeql-action/analyze@v2
|
|
||||||
48
.github/workflows/licenses-update.yml
vendored
Normal file
48
.github/workflows/licenses-update.yml
vendored
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
name: License Report Workflow
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- 'build.gradle'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
generate-license-report:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Check out code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set up JDK 17
|
||||||
|
uses: actions/setup-java@v3
|
||||||
|
with:
|
||||||
|
java-version: '17'
|
||||||
|
distribution: 'adopt'
|
||||||
|
|
||||||
|
- name: Run Gradle Command
|
||||||
|
run: ./gradlew clean generateLicenseReport
|
||||||
|
|
||||||
|
- name: Move and Rename License File
|
||||||
|
run: |
|
||||||
|
mv build/reports/dependency-license/index.json src/main/resources/static/3rdPartyLicenses.json
|
||||||
|
|
||||||
|
- name: Check for Changes
|
||||||
|
id: git-check
|
||||||
|
run: |
|
||||||
|
git add src/main/resources/static/3rdPartyLicenses.json
|
||||||
|
git diff --staged --exit-code || echo "changes=true" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Commit and Push Changes
|
||||||
|
if: env.changes == 'true'
|
||||||
|
run: |
|
||||||
|
git config --global user.name 'Stirling-PDF-Bot'
|
||||||
|
git config --global user.email 'Stirling-PDF-Bot@stirlingtools.com'
|
||||||
|
git commit -m "Update 3rd Party Licenses"
|
||||||
|
git push
|
||||||
|
|
||||||
9
.github/workflows/push-docker.yml
vendored
9
.github/workflows/push-docker.yml
vendored
@@ -6,6 +6,9 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
- main
|
- main
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
jobs:
|
jobs:
|
||||||
push:
|
push:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -139,4 +142,10 @@ jobs:
|
|||||||
cache-to: type=gha,mode=max
|
cache-to: type=gha,mode=max
|
||||||
tags: ${{ steps.meta3.outputs.tags }}
|
tags: ${{ steps.meta3.outputs.tags }}
|
||||||
labels: ${{ steps.meta3.outputs.labels }}
|
labels: ${{ steps.meta3.outputs.labels }}
|
||||||
|
build-args:
|
||||||
|
VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
|
||||||
platforms: linux/amd64,linux/arm64/v8
|
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
|
||||||
|
|||||||
4
.github/workflows/releaseArtifacts.yml
vendored
4
.github/workflows/releaseArtifacts.yml
vendored
@@ -3,7 +3,9 @@ name: Release Artifacts
|
|||||||
on:
|
on:
|
||||||
release:
|
release:
|
||||||
types: [created]
|
types: [created]
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
packages: write
|
||||||
jobs:
|
jobs:
|
||||||
push:
|
push:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
56
.github/workflows/test.yml
vendored
Normal file
56
.github/workflows/test.yml
vendored
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
name: Docker Compose Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'src/**'
|
||||||
|
- '**.gradle'
|
||||||
|
- '!src/main/java/resources/messages*'
|
||||||
|
- 'exampleYmlFiles/**'
|
||||||
|
- 'Dockerfile'
|
||||||
|
- 'Dockerfile**'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout Repository
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Set up Java 17
|
||||||
|
uses: actions/setup-java@v2
|
||||||
|
with:
|
||||||
|
java-version: '17'
|
||||||
|
distribution: 'adopt'
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v1
|
||||||
|
|
||||||
|
- name: Run Docker Compose Tests
|
||||||
|
run: |
|
||||||
|
chmod +x ./gradlew
|
||||||
|
|
||||||
|
- name: Get version number
|
||||||
|
id: versionNumber
|
||||||
|
run: echo "::set-output name=versionNumber::$(./gradlew printVersion --quiet | tail -1)"
|
||||||
|
|
||||||
|
|
||||||
|
- name: Cache Docker layers
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: /tmp/.buildx-cache
|
||||||
|
key: ${{ runner.os }}-buildx-${{ steps.versionNumber.outputs.versionNumber }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-buildx-
|
||||||
|
|
||||||
|
- name: Install Docker Compose
|
||||||
|
run: |
|
||||||
|
sudo curl -L "https://github.com/docker/compose/releases/download/v2.6.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
||||||
|
sudo chmod +x /usr/local/bin/docker-compose
|
||||||
|
|
||||||
|
|
||||||
|
- name: Run Docker Compose Tests
|
||||||
|
run: |
|
||||||
|
chmod +x ./test.sh
|
||||||
|
./test.sh
|
||||||
249
.gitignore
vendored
249
.gitignore
vendored
@@ -1,122 +1,127 @@
|
|||||||
|
|
||||||
|
|
||||||
### Eclipse ###
|
### Eclipse ###
|
||||||
.metadata
|
.metadata
|
||||||
bin/
|
bin/
|
||||||
tmp/
|
tmp/
|
||||||
*.tmp
|
*.tmp
|
||||||
*.bak
|
*.bak
|
||||||
*.swp
|
*.swp
|
||||||
*~.nib
|
*~.nib
|
||||||
local.properties
|
local.properties
|
||||||
.settings/
|
.settings/
|
||||||
.loadpath
|
.loadpath
|
||||||
.recommenders
|
.recommenders
|
||||||
.classpath
|
.classpath
|
||||||
.project
|
.project
|
||||||
version.properties
|
version.properties
|
||||||
pipeline/
|
pipeline/watchedFolders/
|
||||||
|
pipeline/finishedFolders/
|
||||||
#### Stirling-PDF Files ###
|
#### Stirling-PDF Files ###
|
||||||
customFiles/
|
customFiles/
|
||||||
configs/
|
configs/
|
||||||
watchedFolders/
|
watchedFolders/
|
||||||
|
|
||||||
|
|
||||||
# Gradle
|
# Gradle
|
||||||
.gradle
|
.gradle
|
||||||
.lock
|
.lock
|
||||||
|
|
||||||
# External tool builders
|
# External tool builders
|
||||||
.externalToolBuilders/
|
.externalToolBuilders/
|
||||||
|
|
||||||
# Locally stored "Eclipse launch configurations"
|
# Locally stored "Eclipse launch configurations"
|
||||||
*.launch
|
*.launch
|
||||||
|
|
||||||
# PyDev specific (Python IDE for Eclipse)
|
# PyDev specific (Python IDE for Eclipse)
|
||||||
*.pydevproject
|
*.pydevproject
|
||||||
|
|
||||||
# CDT-specific (C/C++ Development Tooling)
|
# CDT-specific (C/C++ Development Tooling)
|
||||||
.cproject
|
.cproject
|
||||||
|
|
||||||
# CDT- autotools
|
# CDT- autotools
|
||||||
.autotools
|
.autotools
|
||||||
|
|
||||||
# Java annotation processor (APT)
|
# Java annotation processor (APT)
|
||||||
.factorypath
|
.factorypath
|
||||||
|
|
||||||
# PDT-specific (PHP Development Tools)
|
# PDT-specific (PHP Development Tools)
|
||||||
.buildpath
|
.buildpath
|
||||||
|
|
||||||
# sbteclipse plugin
|
# sbteclipse plugin
|
||||||
.target
|
.target
|
||||||
|
|
||||||
# Tern plugin
|
# Tern plugin
|
||||||
.tern-project
|
.tern-project
|
||||||
|
|
||||||
# TeXlipse plugin
|
# TeXlipse plugin
|
||||||
.texlipse
|
.texlipse
|
||||||
|
|
||||||
# STS (Spring Tool Suite)
|
# STS (Spring Tool Suite)
|
||||||
.springBeans
|
.springBeans
|
||||||
|
|
||||||
# Code Recommenders
|
# Code Recommenders
|
||||||
.recommenders/
|
.recommenders/
|
||||||
|
|
||||||
# Annotation Processing
|
# Annotation Processing
|
||||||
.apt_generated/
|
.apt_generated/
|
||||||
.apt_generated_test/
|
.apt_generated_test/
|
||||||
|
|
||||||
# Scala IDE specific (Scala & Java development for Eclipse)
|
# Scala IDE specific (Scala & Java development for Eclipse)
|
||||||
.cache-main
|
.cache-main
|
||||||
.scala_dependencies
|
.scala_dependencies
|
||||||
.worksheet
|
.worksheet
|
||||||
|
|
||||||
# Uncomment this line if you wish to ignore the project description file.
|
# Uncomment this line if you wish to ignore the project description file.
|
||||||
# Typically, this file would be tracked if it contains build/dependency configurations:
|
# Typically, this file would be tracked if it contains build/dependency configurations:
|
||||||
#.project
|
#.project
|
||||||
|
|
||||||
### Eclipse Patch ###
|
### Eclipse Patch ###
|
||||||
# Spring Boot Tooling
|
# Spring Boot Tooling
|
||||||
.sts4-cache/
|
.sts4-cache/
|
||||||
|
|
||||||
### Git ###
|
### Git ###
|
||||||
# Created by git for backups. To disable backups in Git:
|
# Created by git for backups. To disable backups in Git:
|
||||||
# $ git config --global mergetool.keepBackup false
|
# $ git config --global mergetool.keepBackup false
|
||||||
*.orig
|
*.orig
|
||||||
|
|
||||||
# Created by git when using merge tools for conflicts
|
# Created by git when using merge tools for conflicts
|
||||||
*.BACKUP.*
|
*.BACKUP.*
|
||||||
*.BASE.*
|
*.BASE.*
|
||||||
*.LOCAL.*
|
*.LOCAL.*
|
||||||
*.REMOTE.*
|
*.REMOTE.*
|
||||||
*_BACKUP_*.txt
|
*_BACKUP_*.txt
|
||||||
*_BASE_*.txt
|
*_BASE_*.txt
|
||||||
*_LOCAL_*.txt
|
*_LOCAL_*.txt
|
||||||
*_REMOTE_*.txt
|
*_REMOTE_*.txt
|
||||||
|
|
||||||
### Java ###
|
### Java ###
|
||||||
# Compiled class file
|
# Compiled class file
|
||||||
*.class
|
*.class
|
||||||
|
|
||||||
# Log file
|
# Log file
|
||||||
*.log
|
*.log
|
||||||
|
|
||||||
# BlueJ files
|
# BlueJ files
|
||||||
*.ctxt
|
*.ctxt
|
||||||
|
|
||||||
# Mobile Tools for Java (J2ME)
|
# Mobile Tools for Java (J2ME)
|
||||||
.mtj.tmp/
|
.mtj.tmp/
|
||||||
|
|
||||||
# Package Files #
|
# Package Files #
|
||||||
*.jar
|
*.jar
|
||||||
*.war
|
*.war
|
||||||
*.nar
|
*.nar
|
||||||
*.ear
|
*.ear
|
||||||
*.zip
|
*.zip
|
||||||
*.tar.gz
|
*.tar.gz
|
||||||
*.rar
|
*.rar
|
||||||
*.db
|
*.db
|
||||||
/build
|
/build
|
||||||
|
|
||||||
/.vscode
|
/.vscode
|
||||||
|
/.idea
|
||||||
|
|
||||||
|
# Ignore Mac DS_Store files
|
||||||
|
.DS_Store
|
||||||
|
**/.DS_Store
|
||||||
40
CONTRIBUTING.md
Normal file
40
CONTRIBUTING.md
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# Contributing to Stirling-PDF
|
||||||
|
|
||||||
|
Thank you for your interest in contributing to Stirling-PDF! There are many ways to contribute other than writing code. For example, reporting bugs, creating suggestions, and adding or modifying translations.
|
||||||
|
|
||||||
|
## Issue Guidelines
|
||||||
|
|
||||||
|
Issues can be used to report bugs, request features, or ask questions. If you have a question, you could also ask us in our [Discord](https://discord.gg/FJUSXUSYec).
|
||||||
|
|
||||||
|
Before opening an issue, please check to make sure someone hasn't already opened an issue about it.
|
||||||
|
|
||||||
|
## Pull Requests
|
||||||
|
|
||||||
|
Before you start working on an issue, please comment on (or create) the issue and wait for it to be assigned to you. If someone has already been assigned but didn't have the time to work on it lately, please communicate with them and ask if they're still working on it. This is to avoid multiple people working on the same issue.
|
||||||
|
|
||||||
|
Once you have been assigned an issue, you can start working on it. When you are ready to submit your changes, open a pull request.
|
||||||
|
For a detailed pull request tutorial, see [this guide](https://www.digitalocean.com/community/tutorials/how-to-create-a-pull-request-on-github).
|
||||||
|
|
||||||
|
Please make sure your Pull Request adheres to the following guidelines:
|
||||||
|
|
||||||
|
- Use the PR template provided.
|
||||||
|
- Keep your Pull Request title succinct, detailed and to the point.
|
||||||
|
- Keep commits atomic. One commit should contain one change. If you want to make multiple changes, submit multiple Pull Requests.
|
||||||
|
- Commits should be clear, concise and easy to understand.
|
||||||
|
- References to the Issue number in the Pull Request and/or Commit message.
|
||||||
|
|
||||||
|
## Translations
|
||||||
|
|
||||||
|
If you would like to add or modify a translation, please see [How to add new languages to Stirling-PDF](HowToAddNewLanguage.md). Also, please create a Pull Request so others can use it!
|
||||||
|
|
||||||
|
## Fixing Bugs or Adding a New Feature
|
||||||
|
|
||||||
|
First, make sure you've read the section [Pull Requests](#pull-requests).
|
||||||
|
|
||||||
|
To build from source, please follow this [Guide](LocalRunGuide.md).
|
||||||
|
|
||||||
|
If, at any point of time, you have a question, please feel free to ask in the same issue thread or in our [Discord](https://discord.gg/FJUSXUSYec).
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
By contributing to this project, you agree that your contributions will be licensed under the [GPL 3 License](LICENSE). You also acknowledge and agree that your contributions will be included in Stirling-PDF and that they can be relicensed in the future under the MPL 2.0 (Mozilla Public License Version 2.0) license.
|
||||||
114
Dockerfile
114
Dockerfile
@@ -1,45 +1,69 @@
|
|||||||
# Use the base image
|
# Main stage
|
||||||
FROM frooodle/stirling-pdf-base:beta4
|
FROM alpine:3.19.1
|
||||||
|
|
||||||
ARG VERSION_TAG
|
# JDK for app
|
||||||
|
RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/apk/repositories && \
|
||||||
# Set Environment Variables
|
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/community" | tee -a /etc/apk/repositories && \
|
||||||
ENV DOCKER_ENABLE_SECURITY=false \
|
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/testing" | tee -a /etc/apk/repositories && \
|
||||||
HOME=/home/stirlingpdfuser \
|
apk add --no-cache \
|
||||||
VERSION_TAG=$VERSION_TAG
|
ca-certificates \
|
||||||
# PUID=1000 \
|
tzdata \
|
||||||
# PGID=1000 \
|
tini \
|
||||||
# UMASK=022 \
|
bash \
|
||||||
|
curl \
|
||||||
|
openjdk17-jre \
|
||||||
# Create user and group
|
# Doc conversion
|
||||||
##RUN groupadd -g $PGID stirlingpdfgroup && \
|
libreoffice@testing \
|
||||||
## useradd -u $PUID -g stirlingpdfgroup -s /bin/sh stirlingpdfuser && \
|
# OCR MY PDF (unpaper for descew and other advanced featues)
|
||||||
## mkdir -p $HOME && chown stirlingpdfuser:stirlingpdfgroup $HOME
|
ocrmypdf \
|
||||||
|
tesseract-ocr-data-eng \
|
||||||
# Set up necessary directories and permissions
|
# CV
|
||||||
RUN mkdir -p /scripts /usr/share/fonts/opentype/noto /usr/share/tesseract-ocr /configs /customFiles
|
py3-opencv \
|
||||||
##&& \
|
# python3/pip
|
||||||
## chown -R stirlingpdfuser:stirlingpdfgroup /scripts /usr/share/fonts/opentype/noto /usr/share/tesseract-ocr /configs /customFiles && \
|
python3 && \
|
||||||
## chown -R stirlingpdfuser:stirlingpdfgroup /usr/share/tesseract-ocr-original
|
wget https://bootstrap.pypa.io/get-pip.py -qO - | python3 - --break-system-packages --no-cache-dir --upgrade && \
|
||||||
|
# uno unoconv and HTML
|
||||||
# Copy necessary files
|
pip install --break-system-packages --no-cache-dir --upgrade unoconv WeasyPrint && \
|
||||||
COPY ./scripts/* /scripts/
|
mv /usr/share/tessdata /usr/share/tessdata-original
|
||||||
COPY src/main/resources/static/fonts/*.ttf /usr/share/fonts/opentype/noto/
|
|
||||||
COPY src/main/resources/static/fonts/*.otf /usr/share/fonts/opentype/noto/
|
|
||||||
COPY build/libs/*.jar app.jar
|
|
||||||
|
ARG VERSION_TAG
|
||||||
# Set font cache and permissions
|
|
||||||
RUN fc-cache -f -v && chmod +x /scripts/init.sh
|
# Set Environment Variables
|
||||||
|
ENV DOCKER_ENABLE_SECURITY=false \
|
||||||
##&& \
|
HOME=/home/stirlingpdfuser \
|
||||||
## chown stirlingpdfuser:stirlingpdfgroup /app.jar && \
|
VERSION_TAG=$VERSION_TAG \
|
||||||
## chmod +x /scripts/init.sh
|
JAVA_TOOL_OPTIONS="$JAVA_TOOL_OPTIONS -XX:MaxRAMPercentage=75"
|
||||||
|
# PUID=1000 \
|
||||||
# Expose necessary ports
|
# PGID=1000 \
|
||||||
EXPOSE 8080
|
# UMASK=022 \
|
||||||
|
|
||||||
# Set user and run command
|
# Copy necessary files
|
||||||
##USER stirlingpdfuser
|
COPY scripts /scripts
|
||||||
ENTRYPOINT ["/scripts/init.sh"]
|
COPY pipeline /pipeline
|
||||||
CMD ["java", "-jar", "/app.jar"]
|
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
|
||||||
|
|
||||||
|
# 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 /configs /logs /customFiles /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 && \
|
||||||
|
# Set font cache and permissions
|
||||||
|
fc-cache -f -v && \
|
||||||
|
chmod +x /scripts/*
|
||||||
|
## chown stirlingpdfuser:stirlingpdfgroup /app.jar && \
|
||||||
|
## chmod +x /scripts/init.sh
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
# Set user and run command
|
||||||
|
##USER stirlingpdfuser
|
||||||
|
ENTRYPOINT ["tini", "--", "/scripts/init.sh"]
|
||||||
|
CMD ["java", "-Dfile.encoding=UTF-8", "-jar", "/app.jar"]
|
||||||
|
|||||||
@@ -1,52 +1,61 @@
|
|||||||
# Build jbig2enc in a separate stage
|
# use alpine
|
||||||
FROM bellsoft/liberica-openjdk-debian:17
|
FROM alpine:3.19.1
|
||||||
RUN apt-get update && \
|
|
||||||
apt-get install -y --no-install-recommends \
|
|
||||||
libreoffice-core-nogui \
|
|
||||||
libreoffice-common \
|
|
||||||
libreoffice-writer-nogui \
|
|
||||||
libreoffice-calc-nogui \
|
|
||||||
libreoffice-impress-nogui \
|
|
||||||
unoconv && \
|
|
||||||
rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
|
ARG VERSION_TAG
|
||||||
|
|
||||||
# Set Environment Variables
|
# Set Environment Variables
|
||||||
ENV PUID=1000 \
|
ENV DOCKER_ENABLE_SECURITY=false \
|
||||||
PGID=1000 \
|
|
||||||
UMASK=022 \
|
|
||||||
DOCKER_ENABLE_SECURITY=false \
|
|
||||||
HOME=/home/stirlingpdfuser \
|
HOME=/home/stirlingpdfuser \
|
||||||
VERSION_TAG=$VERSION_TAG
|
VERSION_TAG=$VERSION_TAG \
|
||||||
|
JAVA_TOOL_OPTIONS="$JAVA_TOOL_OPTIONS -XX:MaxRAMPercentage=75"
|
||||||
# Create user and group
|
# PUID=1000 \
|
||||||
RUN groupadd -g $PGID stirlingpdfgroup && \
|
# PGID=1000 \
|
||||||
useradd -u $PUID -g stirlingpdfgroup -s /bin/sh stirlingpdfuser && \
|
# UMASK=022 \
|
||||||
mkdir -p $HOME && chown stirlingpdfuser:stirlingpdfgroup $HOME
|
|
||||||
|
|
||||||
# Set up necessary directories and permissions
|
|
||||||
RUN mkdir -p /scripts /usr/share/fonts/opentype/noto /configs /customFiles && \
|
|
||||||
chown -R stirlingpdfuser:stirlingpdfgroup /usr/share/fonts/opentype/noto /configs /customFiles
|
|
||||||
|
|
||||||
# Copy necessary files
|
# Copy necessary files
|
||||||
COPY src/main/resources/static/fonts/*.ttf /usr/share/fonts/opentype/noto/
|
COPY scripts/download-security-jar.sh /scripts/download-security-jar.sh
|
||||||
COPY src/main/resources/static/fonts/*.otf /usr/share/fonts/opentype/noto/
|
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
|
COPY build/libs/*.jar app.jar
|
||||||
|
|
||||||
|
RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/apk/repositories && \
|
||||||
|
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/community" | tee -a /etc/apk/repositories && \
|
||||||
|
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/testing" | tee -a /etc/apk/repositories && \
|
||||||
|
apk add --no-cache \
|
||||||
|
ca-certificates \
|
||||||
|
tzdata \
|
||||||
|
tini \
|
||||||
|
bash \
|
||||||
|
curl \
|
||||||
|
openjdk17-jre \
|
||||||
|
# Doc conversion
|
||||||
|
libreoffice@testing \
|
||||||
|
# python and pip
|
||||||
|
python3 && \
|
||||||
|
wget https://bootstrap.pypa.io/get-pip.py -qO - | python3 - --break-system-packages --no-cache-dir --upgrade && \
|
||||||
|
# uno unoconv and HTML
|
||||||
|
pip install --break-system-packages --no-cache-dir --upgrade unoconv WeasyPrint && \
|
||||||
|
# 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
|
||||||
|
mkdir -p /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders && \
|
||||||
|
# chown -R stirlingpdfuser:stirlingpdfgroup /usr/share/fonts/opentype/noto /configs /customFiles
|
||||||
# Set font cache and permissions
|
# Set font cache and permissions
|
||||||
RUN fc-cache -f -v && \
|
fc-cache -f -v && \
|
||||||
chown stirlingpdfuser:stirlingpdfgroup /app.jar
|
chmod +x /scripts/*.sh
|
||||||
|
# chown stirlingpdfuser:stirlingpdfgroup /app.jar
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Expose the application port
|
|
||||||
EXPOSE 8080
|
|
||||||
|
|
||||||
# Set environment variables
|
# Set environment variables
|
||||||
ENV ENDPOINTS_GROUPS_TO_REMOVE=Python,OpenCV,OCRmyPDF
|
ENV ENDPOINTS_GROUPS_TO_REMOVE=OpenCV,OCRmyPDF
|
||||||
ENV DOCKER_ENABLE_SECURITY=false
|
ENV DOCKER_ENABLE_SECURITY=false
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
# Run the application
|
# Run the application
|
||||||
USER stirlingpdfuser
|
#USER stirlingpdfuser
|
||||||
CMD ["java", "-jar", "/app.jar"]
|
ENTRYPOINT ["tini", "--", "/scripts/init-without-ocr.sh"]
|
||||||
|
CMD ["java", "-Dfile.encoding=UTF-8", "-jar", "/app.jar"]
|
||||||
|
|||||||
@@ -1,34 +1,51 @@
|
|||||||
# Build jbig2enc in a separate stage
|
# use alpine
|
||||||
FROM bellsoft/liberica-openjdk-alpine:17
|
FROM alpine:3.19.1
|
||||||
|
|
||||||
|
ARG VERSION_TAG
|
||||||
|
|
||||||
# Set Environment Variables
|
# Set Environment Variables
|
||||||
ENV PUID=1000 \
|
ENV DOCKER_ENABLE_SECURITY=false \
|
||||||
PGID=1000 \
|
|
||||||
UMASK=022 \
|
|
||||||
DOCKER_ENABLE_SECURITY=false \
|
|
||||||
HOME=/home/stirlingpdfuser \
|
HOME=/home/stirlingpdfuser \
|
||||||
VERSION_TAG=$VERSION_TAG
|
VERSION_TAG=$VERSION_TAG \
|
||||||
|
JAVA_TOOL_OPTIONS="$JAVA_TOOL_OPTIONS -XX:MaxRAMPercentage=75"
|
||||||
# Create user and group using Alpine's addgroup and adduser
|
# PUID=1000 \
|
||||||
RUN addgroup -g $PGID stirlingpdfgroup && \
|
# PGID=1000 \
|
||||||
adduser -u $PUID -G stirlingpdfgroup -s /bin/sh -D stirlingpdfuser && \
|
# UMASK=022 \
|
||||||
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
|
|
||||||
|
|
||||||
|
# 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 build/libs/*.jar app.jar
|
COPY build/libs/*.jar app.jar
|
||||||
|
|
||||||
|
# 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 /configs /logs /customFiles && \
|
||||||
# Set font cache and permissions
|
# Set font cache and permissions
|
||||||
RUN chown stirlingpdfuser:stirlingpdfgroup /app.jar
|
#RUN chown stirlingpdfuser:stirlingpdfgroup /app.jar
|
||||||
|
chmod +x /scripts/*.sh && \
|
||||||
# Expose the application port
|
apk add --no-cache \
|
||||||
EXPOSE 8080
|
ca-certificates \
|
||||||
|
tzdata \
|
||||||
|
tini \
|
||||||
|
bash \
|
||||||
|
curl \
|
||||||
|
openjdk17-jre && \
|
||||||
|
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/apk/repositories && \
|
||||||
|
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/community" | tee -a /etc/apk/repositories && \
|
||||||
|
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/testing" | tee -a /etc/apk/repositories
|
||||||
|
|
||||||
# Set environment variables
|
# Set environment variables
|
||||||
ENV ENDPOINTS_GROUPS_TO_REMOVE=CLI
|
ENV ENDPOINTS_GROUPS_TO_REMOVE=CLI
|
||||||
ENV DOCKER_ENABLE_SECURITY=false
|
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
ENTRYPOINT ["tini", "--", "/scripts/init-without-ocr.sh"]
|
||||||
|
|
||||||
# Run the application
|
# Run the application
|
||||||
CMD ["java", "-jar", "/app.jar"]
|
CMD ["java", "-Dfile.encoding=UTF-8", "-jar", "/app.jar"]
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
# Main stage
|
|
||||||
FROM bellsoft/liberica-openjdk-debian:17 AS base
|
|
||||||
RUN apt-get update && \
|
|
||||||
apt-get install -y --no-install-recommends \
|
|
||||||
libreoffice-core-nogui \
|
|
||||||
libreoffice-common \
|
|
||||||
libreoffice-writer-nogui \
|
|
||||||
libreoffice-calc-nogui \
|
|
||||||
libreoffice-impress-nogui \
|
|
||||||
python3-uno \
|
|
||||||
python3-pip \
|
|
||||||
unoconv \
|
|
||||||
pngquant \
|
|
||||||
unpaper \
|
|
||||||
ocrmypdf && \
|
|
||||||
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
|
|
||||||
@@ -1,46 +1,46 @@
|
|||||||
| Operation | PageOps | Convert | Security | Other | CLI | Python | OpenCV | LibreOffice | OCRmyPDF | Java | Javascript |
|
| Operation | PageOps | Convert | Security | Other | CLI | Python | OpenCV | LibreOffice | OCRmyPDF | Java | Javascript |
|
||||||
|---------------------|---------|---------|----------|-------|------|--------|--------|-------------|----------|----------|------------|
|
|---------------------|---------|---------|----------|-------|------|--------|--------|-------------|----------|----------|------------|
|
||||||
| adjust-contrast | ✔️ | | | | | | | | | | ✔️ |
|
| adjust-contrast | ✔️ | | | | | | | | | | ✔️ |
|
||||||
| auto-split-pdf | ✔️ | | | | | | | | | ✔️ | |
|
| auto-split-pdf | ✔️ | | | | | | | | | ✔️ | |
|
||||||
| crop | ✔️ | | | | | | | | | ✔️ | |
|
| crop | ✔️ | | | | | | | | | ✔️ | |
|
||||||
| extract-page | ✔️ | | | | | | | | | ✔️ | |
|
| extract-page | ✔️ | | | | | | | | | ✔️ | |
|
||||||
| merge-pdfs | ✔️ | | | | | | | | | ✔️ | |
|
| merge-pdfs | ✔️ | | | | | | | | | ✔️ | |
|
||||||
| multi-page-layout | ✔️ | | | | | | | | | ✔️ | |
|
| multi-page-layout | ✔️ | | | | | | | | | ✔️ | |
|
||||||
| pdf-organizer | ✔️ | | | | | | | | | ✔️ | ✔️ |
|
| pdf-organizer | ✔️ | | | | | | | | | ✔️ | ✔️ |
|
||||||
| pdf-to-single-page | ✔️ | | | | | | | | | ✔️ | |
|
| pdf-to-single-page | ✔️ | | | | | | | | | ✔️ | |
|
||||||
| remove-pages | ✔️ | | | | | | | | | ✔️ | |
|
| remove-pages | ✔️ | | | | | | | | | ✔️ | |
|
||||||
| rotate-pdf | ✔️ | | | | | | | | | ✔️ | |
|
| rotate-pdf | ✔️ | | | | | | | | | ✔️ | |
|
||||||
| scale-pages | ✔️ | | | | | | | | | ✔️ | |
|
| scale-pages | ✔️ | | | | | | | | | ✔️ | |
|
||||||
| split-pdfs | ✔️ | | | | | | | | | ✔️ | |
|
| split-pdfs | ✔️ | | | | | | | | | ✔️ | |
|
||||||
| file-to-pdf | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
| file-to-pdf | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
||||||
| img-to-pdf | | ✔️ | | | | | | | | ✔️ | |
|
| img-to-pdf | | ✔️ | | | | | | | | ✔️ | |
|
||||||
| pdf-to-html | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
| pdf-to-html | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
||||||
| pdf-to-img | | ✔️ | | | | | | | | ✔️ | |
|
| pdf-to-img | | ✔️ | | | | | | | | ✔️ | |
|
||||||
| pdf-to-pdfa | | ✔️ | | | ✔️ | | | | ✔️ | | |
|
| pdf-to-pdfa | | ✔️ | | | ✔️ | | | | ✔️ | | |
|
||||||
| pdf-to-markdown | | ✔️ | | | | | | | | ✔️ | |
|
| pdf-to-markdown | | ✔️ | | | | | | | | ✔️ | |
|
||||||
| pdf-to-presentation | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
| pdf-to-presentation | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
||||||
| pdf-to-text | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
| pdf-to-text | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
||||||
| pdf-to-word | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
| pdf-to-word | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
||||||
| pdf-to-xml | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
| pdf-to-xml | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
||||||
| xlsx-to-pdf | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
| xlsx-to-pdf | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
||||||
| add-password | | | ✔️ | | | | | | | ✔️ | |
|
| add-password | | | ✔️ | | | | | | | ✔️ | |
|
||||||
| add-watermark | | | ✔️ | | | | | | | ✔️ | |
|
| add-watermark | | | ✔️ | | | | | | | ✔️ | |
|
||||||
| cert-sign | | | ✔️ | | | | | | | ✔️ | |
|
| cert-sign | | | ✔️ | | | | | | | ✔️ | |
|
||||||
| change-permissions | | | ✔️ | | | | | | | ✔️ | |
|
| change-permissions | | | ✔️ | | | | | | | ✔️ | |
|
||||||
| remove-password | | | ✔️ | | | | | | | ✔️ | |
|
| remove-password | | | ✔️ | | | | | | | ✔️ | |
|
||||||
| sanitize-pdf | | | ✔️ | | | | | | | ✔️ | |
|
| sanitize-pdf | | | ✔️ | | | | | | | ✔️ | |
|
||||||
| add-image | | | | ✔️ | | | | | | ✔️ | |
|
| add-image | | | | ✔️ | | | | | | ✔️ | |
|
||||||
| add-page-numbers | | | | ✔️ | | | | | | ✔️ | |
|
| add-page-numbers | | | | ✔️ | | | | | | ✔️ | |
|
||||||
| auto-rename | | | | ✔️ | | | | | | ✔️ | |
|
| auto-rename | | | | ✔️ | | | | | | ✔️ | |
|
||||||
| change-metadata | | | | ✔️ | | | | | | ✔️ | |
|
| change-metadata | | | | ✔️ | | | | | | ✔️ | |
|
||||||
| compare | | | | ✔️ | | | | | | | ✔️ |
|
| compare | | | | ✔️ | | | | | | | ✔️ |
|
||||||
| compress-pdf | | | | ✔️ | ✔️ | | | | ✔️ | | |
|
| compress-pdf | | | | ✔️ | ✔️ | | | | ✔️ | | |
|
||||||
| extract-image-scans | | | | ✔️ | ✔️ | ✔️ | ✔️ | | | | |
|
| extract-image-scans | | | | ✔️ | ✔️ | ✔️ | ✔️ | | | | |
|
||||||
| extract-images | | | | ✔️ | | | | | | ✔️ | |
|
| extract-images | | | | ✔️ | | | | | | ✔️ | |
|
||||||
| flatten | | | | ✔️ | | | | | | | ✔️ |
|
| flatten | | | | ✔️ | | | | | | | ✔️ |
|
||||||
| get-info-on-pdf | | | | ✔️ | | | | | | ✔️ | |
|
| get-info-on-pdf | | | | ✔️ | | | | | | ✔️ | |
|
||||||
| ocr-pdf | | | | ✔️ | ✔️ | | | | ✔️ | | |
|
| ocr-pdf | | | | ✔️ | ✔️ | | | | ✔️ | | |
|
||||||
| remove-blanks | | | | ✔️ | ✔️ | ✔️ | ✔️ | | | | |
|
| remove-blanks | | | | ✔️ | ✔️ | ✔️ | ✔️ | | | | |
|
||||||
| repair | | | | ✔️ | ✔️ | | | ✔️ | | | |
|
| repair | | | | ✔️ | ✔️ | | | ✔️ | | | |
|
||||||
| show-javascript | | | | ✔️ | | | | | | | ✔️ |
|
| show-javascript | | | | ✔️ | | | | | | | ✔️ |
|
||||||
| sign | | | | ✔️ | | | | | | | ✔️ |
|
| sign | | | | ✔️ | | | | | | | ✔️ |
|
||||||
41
FolderScanning.md
Normal file
41
FolderScanning.md
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
## User Guide for Local Directory Scanning and File Processing
|
||||||
|
|
||||||
|
### Whilst Pipelines are in alpha...
|
||||||
|
You must enable this alpha functionality by setting
|
||||||
|
```yaml
|
||||||
|
system:
|
||||||
|
enableAlphaFunctionality: true
|
||||||
|
```
|
||||||
|
To true like in the above for your `/config/settings.yml` file, after restarting Stirling-PDF you should see both UI and folder scanning enabled.
|
||||||
|
|
||||||
|
### Setting Up Watched Folders:
|
||||||
|
- Create a folder where you want your files to be monitored. This is your 'watched folder'.
|
||||||
|
- The default directory for this is `./pipeline/watchedFolders/`
|
||||||
|
- Place any directories you want to be scanned into this folder, this folder should contain multiple folders each for their own tasks and pipelines.
|
||||||
|
|
||||||
|
### Configuring Processing with JSON Files:
|
||||||
|
- In each directory you want processed (e.g `./pipeline/watchedFolders/officePrinter`), include a JSON configuration file.
|
||||||
|
- This JSON file should specify how you want the files in the directory to be handled (e.g., what operations to perform on them) which can be made, configured and downloaded from Stirling-PDF Pipeline interface.r
|
||||||
|
|
||||||
|
### Automatic Scanning and Processing:
|
||||||
|
- The system automatically checks the watched folder every minute for new directories and files to process.
|
||||||
|
- When a directory with a valid JSON configuration file is found, it begins processing the files inside as per the configuration.
|
||||||
|
|
||||||
|
### Processing Steps:
|
||||||
|
- Files in each directory are processed according to the instructions in the JSON file.
|
||||||
|
- This might involve file conversions, data filtering, renaming files, etc. If the output of a step is a zip, this zip will be automatically unzipped as it passes to next process.
|
||||||
|
|
||||||
|
### Results and Output:
|
||||||
|
- After processing, the results are saved in a specified output location. This could be a different folder or location as defined in the JSON file or the default location `./pipeline/finishedFolders/`.
|
||||||
|
- Each processed file is named and organized according to the rules set in the JSON configuration.
|
||||||
|
|
||||||
|
### Completion and Cleanup:
|
||||||
|
- Once processing is complete, the original files in the watched folder's directory are removed.
|
||||||
|
- You can find the processed files in the designated output location.
|
||||||
|
|
||||||
|
### Error Handling:
|
||||||
|
- If there's an error during processing, the system will not delete the original files, allowing you to check and retry if necessary.
|
||||||
|
|
||||||
|
### User Interaction:
|
||||||
|
- As a user, your main tasks are to set up the watched folders, place directories with files for processing, and create the corresponding JSON configuration files.
|
||||||
|
- The system handles the rest, including scanning, processing, and outputting results.
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
<p align="center"><img src="https://raw.githubusercontent.com/Frooodle/Stirling-PDF/main/docs/stirling.png" width="80" ><br><h1 align="center">Stirling-PDF</h1>
|
<p align="center"><img src="https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/main/docs/stirling.png" width="80" ><br><h1 align="center">Stirling-PDF</h1>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
||||||
@@ -8,15 +8,15 @@ 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
|
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/languages.html
|
https://github.com/Stirling-Tools/Stirling-PDF/blob/main/src/main/resources/templates/fragments/languages.html
|
||||||
and add a flag svg file to
|
and add a flag svg file to
|
||||||
https://github.com/Frooodle/Stirling-PDF/tree/main/src/main/resources/static/images/flags
|
https://github.com/Stirling-Tools/Stirling-PDF/tree/main/src/main/resources/static/images/flags
|
||||||
Any SVG flags are fine, i got most of mine from [here](https://flagicons.lipis.dev/)
|
Any SVG flags are fine, i got most of mine from [here](https://flagicons.lipis.dev/)
|
||||||
If your language isnt represented by a flag just find whichever closely matches it, such as for Arabic i chose Saudi Arabia
|
If your language isnt represented by a flag just find whichever closely matches it, such as for Arabic i chose Saudi Arabia
|
||||||
|
|
||||||
|
|
||||||
For example to add Polish you would add
|
For example to add Polish you would add
|
||||||
```
|
```html
|
||||||
<a class="dropdown-item lang_dropdown-item" href="" data-language-code="pl_PL">
|
<a class="dropdown-item lang_dropdown-item" href="" data-language-code="pl_PL">
|
||||||
<img src="images/flags/pl.svg" alt="icon" width="20" height="15"> Polski
|
<img src="images/flags/pl.svg" alt="icon" width="20" height="15"> Polski
|
||||||
</a>
|
</a>
|
||||||
@@ -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
|
Start by copying the existing english property file
|
||||||
|
|
||||||
[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)
|
[https://github.com/Stirling-Tools/Stirling-PDF/blob/main/src/main/resources/messages_en_GB.properties](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/src/main/resources/messages_en_GB.properties)
|
||||||
|
|
||||||
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
|
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
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
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
|
## How does the OCR Work
|
||||||
Stirling-PDF uses [OCRmyPDF](https://github.com/ocrmypdf/OCRmyPDF) which in turn uses tesseract for its text recognition.
|
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!
|
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
|
### Installing Language Packs
|
||||||
|
|
||||||
1. Download the desired language pack(s) by selecting the `.traineddata` file(s) for the language(s) you need.
|
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/tessdata`
|
||||||
|
|
||||||
# DO NOT REMOVE EXISTING ENG.TRAINEDDATA, ITS REQUIRED.
|
# DO NOT REMOVE EXISTING ENG.TRAINEDDATA, IT'S REQUIRED.
|
||||||
|
|
||||||
#### Docker
|
#### Docker
|
||||||
|
|
||||||
@@ -34,14 +37,14 @@ services:
|
|||||||
your_service_name:
|
your_service_name:
|
||||||
image: your_docker_image_name
|
image: your_docker_image_name
|
||||||
volumes:
|
volumes:
|
||||||
- /location/of/trainingData:/usr/share/tesseract-ocr/4.00/tessdata
|
- /location/of/trainingData:/usr/share/tessdata
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
#### Docker run
|
#### Docker run
|
||||||
Add the following to your existing docker run command
|
Add the following to your existing docker run command
|
||||||
```bash
|
```bash
|
||||||
-v /location/of/trainingData:/usr/share/tesseract-ocr/4.00/tessdata
|
-v /location/of/trainingData:/usr/share/tessdata
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Non-Docker
|
#### Non-Docker
|
||||||
|
|||||||
76
Jenkinsfile
vendored
76
Jenkinsfile
vendored
@@ -1,33 +1,45 @@
|
|||||||
pipeline {
|
pipeline {
|
||||||
agent any
|
agent any
|
||||||
stages {
|
stages {
|
||||||
stage('Build') {
|
stage('Build') {
|
||||||
steps {
|
steps {
|
||||||
sh 'chmod 755 gradlew'
|
sh 'chmod 755 gradlew'
|
||||||
sh './gradlew build'
|
sh './gradlew build'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
stage('Docker Build') {
|
stage('Docker Build') {
|
||||||
steps {
|
steps {
|
||||||
script {
|
script {
|
||||||
def appVersion = sh(returnStdout: true, script: './gradlew printVersion -q').trim()
|
def appVersion = sh(returnStdout: true, script: './gradlew printVersion -q').trim()
|
||||||
def image = "frooodle/s-pdf:$appVersion"
|
def image = "frooodle/s-pdf:$appVersion"
|
||||||
sh "docker build -t $image ."
|
sh "docker build -t $image ."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
stage('Docker Push') {
|
stage('Docker Push') {
|
||||||
steps {
|
steps {
|
||||||
script {
|
script {
|
||||||
def appVersion = sh(returnStdout: true, script: './gradlew printVersion -q').trim()
|
def appVersion = sh(returnStdout: true, script: './gradlew printVersion -q').trim()
|
||||||
def image = "frooodle/s-pdf:$appVersion"
|
def image = "frooodle/s-pdf:$appVersion"
|
||||||
withCredentials([string(credentialsId: 'docker_hub_access_token', variable: 'DOCKER_HUB_ACCESS_TOKEN')]) {
|
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"
|
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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.
|
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.
|
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.
|
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
|
### Step 1: Prerequisites
|
||||||
|
|
||||||
Install the following software, if not already installed:
|
Install the following software, if not already installed:
|
||||||
@@ -18,7 +20,7 @@ Install the following software, if not already installed:
|
|||||||
|
|
||||||
- Git
|
- Git
|
||||||
|
|
||||||
- Python 3 (with pip)
|
- Python 3.8 (with pip)
|
||||||
|
|
||||||
- Make
|
- Make
|
||||||
|
|
||||||
@@ -93,21 +95,21 @@ For Debian-based systems, you can use the following command:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo apt-get install -y libreoffice-writer libreoffice-calc libreoffice-impress unpaper ocrmypdf
|
sudo apt-get install -y libreoffice-writer libreoffice-calc libreoffice-impress unpaper ocrmypdf
|
||||||
pip3 install uno opencv-python-headless unoconv pngquant
|
pip3 install uno opencv-python-headless unoconv pngquant WeasyPrint
|
||||||
```
|
```
|
||||||
|
|
||||||
For Fedora:
|
For Fedora:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo dnf install -y libreoffice-writer libreoffice-calc libreoffice-impress unpaper ocrmypdf
|
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
|
### Step 4: Clone and Build Stirling-PDF
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd ~/.git &&\
|
cd ~/.git &&\
|
||||||
git clone https://github.com/Frooodle/Stirling-PDF.git &&\
|
git clone https://github.com/Stirling-Tools/Stirling-PDF.git &&\
|
||||||
cd Stirling-PDF &&\
|
cd Stirling-PDF &&\
|
||||||
chmod +x ./gradlew &&\
|
chmod +x ./gradlew &&\
|
||||||
./gradlew build
|
./gradlew build
|
||||||
@@ -137,7 +139,7 @@ Easiest is to use the langpacks provided by your repositories. Skip the other st
|
|||||||
Manual:
|
Manual:
|
||||||
|
|
||||||
1. Download the desired language pack(s) by selecting the `.traineddata` file(s) for the language(s) you need.
|
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/tessdata`
|
||||||
3.
|
3.
|
||||||
Please view [OCRmyPDF install guide](https://ocrmypdf.readthedocs.io/en/latest/installation.html) for more info.
|
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.
|
**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
|
```bash
|
||||||
./gradlew bootRun
|
./gradlew bootRun
|
||||||
or
|
or
|
||||||
java -jar build/libs/app.jar
|
java -jar /opt/Stirling-PDF/Stirling-PDF-*.jar
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 8: Adding a Desktop icon
|
### Step 8: Adding a Desktop icon
|
||||||
@@ -200,6 +202,64 @@ EOF
|
|||||||
|
|
||||||
Note: Currently the app will run in the background until manually closed.
|
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.
|
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.
|
||||||
|
|||||||
42
PipelineFeature.md
Normal file
42
PipelineFeature.md
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# Pipeline Configuration and Usage Tutorial
|
||||||
|
|
||||||
|
## Whilst Pipelines are in alpha...
|
||||||
|
You must enable this alpha functionality by setting
|
||||||
|
```yaml
|
||||||
|
system:
|
||||||
|
enableAlphaFunctionality: true
|
||||||
|
```
|
||||||
|
To true like in the above for your `/config/settings.yml` file, after restarting Stirling-PDF you should see both UI and folder scanning enabled.
|
||||||
|
|
||||||
|
|
||||||
|
## Steps to Configure and Use Your Pipeline
|
||||||
|
|
||||||
|
1. **Access Configuration**
|
||||||
|
- Upon entering the screen, click on the **Configure** button.
|
||||||
|
|
||||||
|
2. **Enter Pipeline Name**
|
||||||
|
- Provide a name for your pipeline in the designated field.
|
||||||
|
|
||||||
|
3. **Select Operations**
|
||||||
|
- Choose the operations for your pipeline (e.g., **Split Pages**), then click **Add Operation**.
|
||||||
|
|
||||||
|
4. **Configure Operation Settings**
|
||||||
|
- Input the necessary settings for each added operation. Settings are highlighted in yellow if customization is needed.
|
||||||
|
|
||||||
|
5. **Add More Operations**
|
||||||
|
- You can add and adjust the order of multiple operations. Ensure each operation's settings are customized.
|
||||||
|
|
||||||
|
6. **Save Settings**
|
||||||
|
- Click **Save Operation Settings** after customizing settings for each operation.
|
||||||
|
|
||||||
|
7. **Validate Pipeline**
|
||||||
|
- Use the **Validation** button to check your pipeline. A green indicator signifies correct setup; a pop-out error indicates issues.
|
||||||
|
|
||||||
|
8. **Download Pipeline Configuration**
|
||||||
|
- To use the configuration for folder scanning (or save it for future use and reupload it), you can also download a JSON file in this menu. You can also pre-load this for future use by placing it in ``/pipeline/defaultWebUIConfigs/``. It will then appear in the dropdown menu for all users to use.
|
||||||
|
|
||||||
|
9. **Submit Files for Processing**
|
||||||
|
- If your pipeline is correctly set up close the configure menu, input the files and hit **Submit**.
|
||||||
|
|
||||||
|
10. **Note on Web UI Limitations**
|
||||||
|
- The current web UI version does not support operations that require multiple different types of inputs, such as adding a separate image to a PDF.
|
||||||
191
README.md
191
README.md
@@ -1,131 +1,129 @@
|
|||||||
<p align="center"><img src="https://raw.githubusercontent.com/Frooodle/Stirling-PDF/main/docs/stirling.png" width="80" ><br><h1 align="center">Stirling-PDF</h1>
|
<p align="center"><img src="https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/main/docs/stirling.png" width="80" ><br><h1 align="center">Stirling-PDF</h1>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
[](https://hub.docker.com/r/frooodle/s-pdf)
|
[](https://hub.docker.com/r/frooodle/s-pdf)
|
||||||
[](https://discord.gg/Cn8pWhQRxZ)
|
[](https://discord.gg/Cn8pWhQRxZ)
|
||||||
[](https://github.com/Frooodle/Stirling-PDF/)
|
[](https://github.com/Stirling-Tools/Stirling-PDF/)
|
||||||
[](https://github.com/Frooodle/stirling-pdf)
|
[](https://github.com/Stirling-Tools/stirling-pdf)
|
||||||
[](https://www.paypal.com/paypalme/froodleplex)
|
[](https://www.paypal.com/paypalme/froodleplex)
|
||||||
[](https://github.com/sponsors/Frooodle)
|
[](https://github.com/sponsors/Frooodle)
|
||||||
|
|
||||||
[](https://cloud.digitalocean.com/apps/new?repo=https://github.com/Frooodle/Stirling-PDF/tree/digitalOcean&refcode=c3210994b1af)
|
[](https://cloud.digitalocean.com/apps/new?repo=https://github.com/Stirling-Tools/Stirling-PDF/tree/digitalOcean&refcode=c3210994b1af)
|
||||||
|
|
||||||
This is a powerful locally hosted web based PDF manipulation tool using docker that allows you to perform various operations on PDF files, such as splitting merging, converting, reorganizing, adding images, rotating, compressing, and more. This locally hosted web application started as a 100% ChatGPT-made application and has evolved to include a wide range of features to handle all your PDF needs.
|
This is a powerful locally hosted web based PDF manipulation tool using docker that allows you to perform various operations on PDF files, such as splitting merging, converting, reorganizing, adding images, rotating, compressing, and more. This locally hosted web application started as a 100% ChatGPT-made application and has evolved to include a wide range of features to handle all your PDF needs.
|
||||||
|
|
||||||
Stirling PDF makes no outbound calls for any record keeping or tracking.
|
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.
|
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.
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
- Dark mode support.
|
- Dark mode support.
|
||||||
- Custom download options (see [here](https://github.com/Frooodle/Stirling-PDF/blob/main/images/settings.png) for example)
|
- Custom download options (see [here](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/images/settings.png) for example)
|
||||||
- Parallel file processing and downloads
|
- Parallel file processing and downloads
|
||||||
- API for integration with external scripts
|
- 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)
|
- Optional Login and Authentication support (see [here](https://github.com/Stirling-Tools/Stirling-PDF/tree/main#login-authentication) for documentation)
|
||||||
|
|
||||||
|
|
||||||
## **PDF Features**
|
## **PDF Features**
|
||||||
|
|
||||||
### **Page Operations**
|
### **Page Operations**
|
||||||
- Full interactive GUI for merging/splitting/rotating/moving PDFs and their pages.
|
- 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)
|
||||||
- Merge multiple PDFs together into a single resultant file.
|
- 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.
|
||||||
- Reorganize PDF pages into different orders.
|
- Split PDFs into multiple files at specified page numbers or extract all pages as individual files.
|
||||||
- Rotate PDFs in 90-degree increments.
|
- Reorganize PDF pages into different orders.
|
||||||
- Remove pages.
|
- Rotate PDFs in 90-degree increments.
|
||||||
- Multi-page layout (Format PDFs into a multi-paged page).
|
- Remove pages.
|
||||||
- Scale page contents size by set %.
|
- Multi-page layout (Format PDFs into a multi-paged page).
|
||||||
- Adjust Contrast.
|
- Scale page contents size by set %.
|
||||||
- Crop PDF.
|
- Adjust Contrast.
|
||||||
- Auto Split PDF (With physically scanned page dividers).
|
- Crop PDF.
|
||||||
- Extract page(s).
|
- Auto Split PDF (With physically scanned page dividers).
|
||||||
- Convert PDF to a single page.
|
- Extract page(s).
|
||||||
|
- Convert PDF to a single page.
|
||||||
|
|
||||||
### **Conversion Operations**
|
### **Conversion Operations**
|
||||||
- Convert PDFs to and from images.
|
- Convert PDFs to and from images.
|
||||||
- Convert any common file to PDF (using LibreOffice).
|
- Convert any common file to PDF (using LibreOffice).
|
||||||
- Convert PDF to Word/Powerpoint/Others (using LibreOffice).
|
- Convert PDF to Word/Powerpoint/Others (using LibreOffice).
|
||||||
- Convert HTML to PDF.
|
- Convert HTML to PDF.
|
||||||
- URL to PDF.
|
- URL to PDF.
|
||||||
- Markdown to PDF.
|
- Markdown to PDF.
|
||||||
|
|
||||||
### **Security & Permissions**
|
### **Security & Permissions**
|
||||||
- Add and remove passwords.
|
- Add and remove passwords.
|
||||||
- Change/set PDF Permissions.
|
- Change/set PDF Permissions.
|
||||||
- Add watermark(s).
|
- Add watermark(s).
|
||||||
- Certify/sign PDFs.
|
- Certify/sign PDFs.
|
||||||
- Sanitize PDFs.
|
- Sanitize PDFs.
|
||||||
- Auto-redact text.
|
- Auto-redact text.
|
||||||
|
|
||||||
### **Other Operations**
|
### **Other Operations**
|
||||||
- Add/Generate/Write signatures.
|
- Add/Generate/Write signatures.
|
||||||
- Repair PDFs.
|
- Repair PDFs.
|
||||||
- Detect and remove blank pages.
|
- Detect and remove blank pages.
|
||||||
- Compare 2 PDFs and show differences in text.
|
- Compare 2 PDFs and show differences in text.
|
||||||
- Add images to PDFs.
|
- Add images to PDFs.
|
||||||
- Compress PDFs to decrease their filesize (Using OCRMyPDF).
|
- Compress PDFs to decrease their filesize (Using OCRMyPDF).
|
||||||
- Extract images from PDF.
|
- Extract images from PDF.
|
||||||
- Extract images from Scans.
|
- Extract images from Scans.
|
||||||
- Add page numbers.
|
- Add page numbers.
|
||||||
- Auto rename file by detecting PDF header text.
|
- Auto rename file by detecting PDF header text.
|
||||||
- OCR on PDF (Using OCRMyPDF).
|
- OCR on PDF (Using OCRMyPDF).
|
||||||
- PDF/A conversion (Using OCRMyPDF).
|
- PDF/A conversion (Using OCRMyPDF).
|
||||||
- Edit metadata.
|
- Edit metadata.
|
||||||
- Flatten PDFs.
|
- Flatten PDFs.
|
||||||
- Get all information on a PDF to view or export as JSON.
|
- 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 [groups.md](https://github.com/Frooodle/Stirling-PDF/blob/main/Groups.md)
|
For a overview of the tasks and the technology each uses please view [Endpoint-groups.md](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/Endpoint-groups.md)
|
||||||
Hosted instance/demo of the app can be seen [here](https://pdf.adminforge.de/) hosted by the team at adminforge.de
|
Demo of the app is available [here](https://stirlingpdf.io). username: demo, password: demo
|
||||||
|
|
||||||
## Technologies used
|
## Technologies used
|
||||||
- Spring Boot + Thymeleaf
|
- Spring Boot + Thymeleaf
|
||||||
- PDFBox
|
- [PDFBox](https://github.com/apache/pdfbox/tree/trunk)
|
||||||
- [LibreOffice](https://www.libreoffice.org/discover/libreoffice/) for advanced conversions
|
- [LibreOffice](https://www.libreoffice.org/discover/libreoffice/) for advanced conversions
|
||||||
- [OcrMyPdf](https://github.com/ocrmypdf/OCRmyPDF)
|
- [OcrMyPdf](https://github.com/ocrmypdf/OCRmyPDF)
|
||||||
- HTML, CSS, JavaScript
|
- HTML, CSS, JavaScript
|
||||||
- Docker
|
- Docker
|
||||||
- PDF.js
|
- [PDF.js](https://github.com/mozilla/pdf.js)
|
||||||
- PDF-LIB.js
|
- [PDF-LIB.js](https://github.com/Hopding/pdf-lib)
|
||||||
|
|
||||||
## How to use
|
## How to use
|
||||||
|
|
||||||
### Locally
|
### Locally
|
||||||
Please view https://github.com/Frooodle/Stirling-PDF/blob/main/LocalRunGuide.md
|
Please view https://github.com/Stirling-Tools/Stirling-PDF/blob/main/LocalRunGuide.md
|
||||||
|
|
||||||
### Docker
|
### Docker / Podman
|
||||||
https://hub.docker.com/r/frooodle/s-pdf
|
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)
|
To see what the different versions offer please look at our [version mapping](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/Version-groups.md)
|
||||||
For people that don't mind about space optimization just use the latest tag.
|
For people that don't mind about space optimization just use the latest tag.
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
Docker Run
|
Docker Run
|
||||||
```
|
```bash
|
||||||
docker run -d \
|
docker run -d \
|
||||||
-p 8080:8080 \
|
-p 8080:8080 \
|
||||||
-v /location/of/trainingData:/usr/share/tesseract-ocr/4.00/tessdata \
|
-v /location/of/trainingData:/usr/share/tessdata \
|
||||||
-v /location/of/extraConfigs:/configs \
|
-v /location/of/extraConfigs:/configs \
|
||||||
|
-v /location/of/logs:/logs \
|
||||||
-e DOCKER_ENABLE_SECURITY=false \
|
-e DOCKER_ENABLE_SECURITY=false \
|
||||||
--name stirling-pdf \
|
--name stirling-pdf \
|
||||||
frooodle/s-pdf:latest
|
frooodle/s-pdf:latest
|
||||||
|
|
||||||
|
|
||||||
Can also add these for customisation but are not required
|
Can also add these for customisation but are not required
|
||||||
|
|
||||||
-v /location/of/customFiles:/customFiles \
|
-v /location/of/customFiles:/customFiles \
|
||||||
```
|
```
|
||||||
Docker Compose
|
Docker Compose
|
||||||
```
|
```yaml
|
||||||
version: '3.3'
|
version: '3.3'
|
||||||
services:
|
services:
|
||||||
stirling-pdf:
|
stirling-pdf:
|
||||||
@@ -133,26 +131,30 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- '8080:8080'
|
- '8080:8080'
|
||||||
volumes:
|
volumes:
|
||||||
- /location/of/trainingData:/usr/share/tesseract-ocr/4.00/tessdata #Required for extra OCR languages
|
- /location/of/trainingData:/usr/share/tessdata #Required for extra OCR languages
|
||||||
- /location/of/extraConfigs:/configs
|
- /location/of/extraConfigs:/configs
|
||||||
# - /location/of/customFiles:/customFiles/
|
# - /location/of/customFiles:/customFiles/
|
||||||
|
# - /location/of/logs:/logs/
|
||||||
environment:
|
environment:
|
||||||
- DOCKER_ENABLE_SECURITY=false
|
- DOCKER_ENABLE_SECURITY=false
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Note: Podman is CLI-compatible with Docker, so simply replace "docker" with "podman".
|
||||||
|
|
||||||
## Enable OCR/Compression feature
|
## Enable OCR/Compression feature
|
||||||
Please view https://github.com/Frooodle/Stirling-PDF/blob/main/HowToUseOCR.md
|
Please view https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToUseOCR.md
|
||||||
|
|
||||||
## Want to add your own language?
|
## Supported Languages
|
||||||
Stirling PDF currently supports 18!
|
|
||||||
|
Stirling PDF currently supports 26!
|
||||||
- English (English) (en_GB)
|
- English (English) (en_GB)
|
||||||
- English (US) (en_US)
|
- English (US) (en_US)
|
||||||
- Arabic (العربية) (ar_AR)
|
- Arabic (العربية) (ar_AR)
|
||||||
- German (Deutsch) (de_DE)
|
- German (Deutsch) (de_DE)
|
||||||
- French (Français) (fr_FR)
|
- French (Français) (fr_FR)
|
||||||
- Spanish (Español) (es_ES)
|
- Spanish (Español) (es_ES)
|
||||||
- Chinese (简体中文) (zh_CN)
|
- Simplified Chinese (简体中文) (zh_CN)
|
||||||
|
- Traditional Chinese (繁體中文) (zh_TW)
|
||||||
- Catalan (Català) (ca_CA)
|
- Catalan (Català) (ca_CA)
|
||||||
- Italian (Italiano) (it_IT)
|
- Italian (Italiano) (it_IT)
|
||||||
- Swedish (Svenska) (sv_SE)
|
- Swedish (Svenska) (sv_SE)
|
||||||
@@ -164,16 +166,17 @@ Stirling PDF currently supports 18!
|
|||||||
- Basque (Euskara) (eu_ES)
|
- Basque (Euskara) (eu_ES)
|
||||||
- Japanese (日本語) (ja_JP)
|
- Japanese (日本語) (ja_JP)
|
||||||
- Dutch (Nederlands) (nl_NL)
|
- Dutch (Nederlands) (nl_NL)
|
||||||
|
- Greek (el_GR)
|
||||||
|
- Turkish (Türkçe) (tr_TR)
|
||||||
|
- Indonesia (Bahasa Indonesia) (id_ID)
|
||||||
|
- Hindi (हिंदी) (hi_IN)
|
||||||
|
- Hungarian (Magyar) (hu_HU)
|
||||||
|
- Bulgarian (Български) (bg_BG)
|
||||||
|
- Sebian Latin alphabet (Srpski) (sr-Latn-RS)
|
||||||
|
|
||||||
If you want to add your own language to Stirling-PDF please refer
|
## Contributing (creating issues, translations, fixing bugs, etc.)
|
||||||
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!
|
|
||||||
|
|
||||||
## 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.
|
|
||||||
|
|
||||||
|
Please see our [Contributing Guide](CONTRIBUTING.md)!
|
||||||
|
|
||||||
## Customisation
|
## Customisation
|
||||||
Stirling PDF allows easy customization of the app.
|
Stirling PDF allows easy customization of the app.
|
||||||
@@ -187,7 +190,7 @@ This file is located in the ``/configs`` directory and follows standard YAML for
|
|||||||
|
|
||||||
Environment variables are also supported and would override the settings file
|
Environment variables are also supported and would override the settings file
|
||||||
For example in the settings.yml you have
|
For example in the settings.yml you have
|
||||||
```
|
```yaml
|
||||||
system:
|
system:
|
||||||
defaultLocale: 'en-US'
|
defaultLocale: 'en-US'
|
||||||
```
|
```
|
||||||
@@ -195,7 +198,7 @@ system:
|
|||||||
To have this via an environment variable you would have ``SYSTEM_DEFAULTLOCALE``
|
To have this via an environment variable you would have ``SYSTEM_DEFAULTLOCALE``
|
||||||
|
|
||||||
The Current list of settings is
|
The Current list of settings is
|
||||||
```
|
```yaml
|
||||||
security:
|
security:
|
||||||
enableLogin: false # set to 'true' to enable login
|
enableLogin: false # set to 'true' to enable login
|
||||||
csrfDisabled: true
|
csrfDisabled: true
|
||||||
@@ -218,24 +221,26 @@ metrics:
|
|||||||
enabled: true # 'true' to enable Info APIs endpoints (view http://localhost:8080/swagger-ui/index.html#/API to learn more), 'false' to disable
|
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
|
### 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/groups.md)
|
- Endpoints. Currently, the endpoints ENDPOINTS_TO_REMOVE and GROUPS_TO_REMOVE can include comma separate lists of endpoints and groups to disable as example ENDPOINTS_TO_REMOVE=img-to-pdf,remove-pages would disable both image-to-pdf and remove pages, GROUPS_TO_REMOVE=LibreOffice Would disable all things that use LibreOffice. You can see a list of all endpoints and groups [here](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/Endpoint-groups.md)
|
||||||
- customStaticFilePath. Customise static files such as the app logo by placing files in the /customFiles/static/ directory. An example of customising app logo is placing a /customFiles/static/favicon.svg to override current SVG. This can be used to change any images/icons/css/fonts/js etc in Stirling-PDF
|
- 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
|
### Environment only parameters
|
||||||
- ``SYSTEM_ROOTURIPATH`` ie set to ``pdf-app`` to Set the application's root URI tp ``localhost:8080/pdf-app``
|
- ``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
|
- ``SYSTEM_CONNECTIONTIMEOUTMINUTES`` to set custom connection timeout values
|
||||||
- ``DOCKER_ENABLE_SECURITY`` to tell docker to download security jar (required as true for auth login)
|
- ``DOCKER_ENABLE_SECURITY`` to tell docker to download security jar (required as true for auth login)
|
||||||
|
|
||||||
## API
|
## 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
|
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)
|
[here](https://app.swaggerhub.com/apis-docs/Stirling-Tools/Stirling-PDF/) or navigate to /swagger-ui/index.html of your stirling-pdf instance for your versions documentation (Or by following the API button in your settings of Stirling-PDF)
|
||||||
|
|
||||||
|
|
||||||
## Login authentication
|
## Login authentication
|
||||||
### Prerequisites:
|

|
||||||
|
### Prerequisites:
|
||||||
- User must have the folder ./configs volumed within docker so that it is retained during updates.
|
- 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.
|
- Docker uses must download the security jar version by setting ``DOCKER_ENABLE_SECURITY`` to ``true`` in environment variables.
|
||||||
- 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.
|
- 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.
|
Once the above has been done, on restart, a new stirling-pdf-DB.mv.db will show if everything worked.
|
||||||
|
|
||||||
@@ -254,11 +259,13 @@ For API usage you must provide a header with 'X-API-Key' and the associated API
|
|||||||
- Progress bar/Tracking
|
- Progress bar/Tracking
|
||||||
- Full custom logic pipelines to combine multiple operations together.
|
- Full custom logic pipelines to combine multiple operations together.
|
||||||
- Folder support with auto scanning to perform operations on
|
- Folder support with auto scanning to perform operations on
|
||||||
- Redact text (Via UI not just automated way)
|
- Redact text (Via UI not just automated way)
|
||||||
- Add Forms
|
- Add Forms
|
||||||
- Annotations
|
- Multi page layout (Stich PDF pages together) support x rows y columns and custom page sizing
|
||||||
- Multi page layout (Stich PDF pages together) support x rows y columns and custom page sizing
|
- Fill forms mannual and automatic
|
||||||
- Fill forms mannual and automatic
|
|
||||||
|
|
||||||
### Q2: Why is my application downloading .htm files?
|
### Q2: 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.
|
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;``
|
||||||
|
|||||||
@@ -1,59 +1,64 @@
|
|||||||
|Technology | Ultra-Lite | Lite | Full |
|
|Technology | Ultra-Lite | Lite | Full |
|
||||||
|----------------|:----------:|:----:|:----:|
|
|----------------|:----------:|:----:|:----:|
|
||||||
| Java | ✔️ | ✔️ | ✔️ |
|
| Java | ✔️ | ✔️ | ✔️ |
|
||||||
| JavaScript | ✔️ | ✔️ | ✔️ |
|
| JavaScript | ✔️ | ✔️ | ✔️ |
|
||||||
| Libre | | ✔️ | ✔️ |
|
| Libre | | ✔️ | ✔️ |
|
||||||
| Python | | | ✔️ |
|
| Python | | | ✔️ |
|
||||||
| OpenCV | | | ✔️ |
|
| OpenCV | | | ✔️ |
|
||||||
| OCRmyPDF | | | ✔️ |
|
| OCRmyPDF | | | ✔️ |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Operation | Ultra-Lite | Lite | Full
|
Operation | Ultra-Lite | Lite | Full
|
||||||
--------------------|------------|------|-----
|
--------------------|------------|------|-----
|
||||||
add-page-numbers | ✔️ | ✔️ | ✔️
|
add-page-numbers | ✔️ | ✔️ | ✔️
|
||||||
add-password | ✔️ | ✔️ | ✔️
|
add-password | ✔️ | ✔️ | ✔️
|
||||||
add-image | ✔️ | ✔️ | ✔️
|
add-image | ✔️ | ✔️ | ✔️
|
||||||
add-watermark | ✔️ | ✔️ | ✔️
|
add-watermark | ✔️ | ✔️ | ✔️
|
||||||
adjust-contrast | ✔️ | ✔️ | ✔️
|
adjust-contrast | ✔️ | ✔️ | ✔️
|
||||||
auto-split-pdf | ✔️ | ✔️ | ✔️
|
auto-split-pdf | ✔️ | ✔️ | ✔️
|
||||||
auto-rename | ✔️ | ✔️ | ✔️
|
auto-redact | ✔️ | ✔️ | ✔️
|
||||||
cert-sign | ✔️ | ✔️ | ✔️
|
auto-rename | ✔️ | ✔️ | ✔️
|
||||||
crop | ✔️ | ✔️ | ✔️
|
cert-sign | ✔️ | ✔️ | ✔️
|
||||||
change-metadata | ✔️ | ✔️ | ✔️
|
crop | ✔️ | ✔️ | ✔️
|
||||||
change-permissions | ✔️ | ✔️ | ✔️
|
change-metadata | ✔️ | ✔️ | ✔️
|
||||||
compare | ✔️ | ✔️ | ✔️
|
change-permissions | ✔️ | ✔️ | ✔️
|
||||||
extract-page | ✔️ | ✔️ | ✔️
|
compare | ✔️ | ✔️ | ✔️
|
||||||
extract-images | ✔️ | ✔️ | ✔️
|
extract-page | ✔️ | ✔️ | ✔️
|
||||||
flatten | ✔️ | ✔️ | ✔️
|
extract-images | ✔️ | ✔️ | ✔️
|
||||||
get-info-on-pdf | ✔️ | ✔️ | ✔️
|
flatten | ✔️ | ✔️ | ✔️
|
||||||
img-to-pdf | ✔️ | ✔️ | ✔️
|
get-info-on-pdf | ✔️ | ✔️ | ✔️
|
||||||
markdown-to-pdf | ✔️ | ✔️ | ✔️
|
img-to-pdf | ✔️ | ✔️ | ✔️
|
||||||
merge-pdfs | ✔️ | ✔️ | ✔️
|
markdown-to-pdf | ✔️ | ✔️ | ✔️
|
||||||
multi-page-layout | ✔️ | ✔️ | ✔️
|
merge-pdfs | ✔️ | ✔️ | ✔️
|
||||||
pdf-organizer | ✔️ | ✔️ | ✔️
|
multi-page-layout | ✔️ | ✔️ | ✔️
|
||||||
pdf-to-img | ✔️ | ✔️ | ✔️
|
overlay-pdf | ✔️ | ✔️ | ✔️
|
||||||
pdf-to-single-page | ✔️ | ✔️ | ✔️
|
pdf-organizer | ✔️ | ✔️ | ✔️
|
||||||
remove-pages | ✔️ | ✔️ | ✔️
|
pdf-to-csv | ✔️ | ✔️ | ✔️
|
||||||
remove-password | ✔️ | ✔️ | ✔️
|
pdf-to-img | ✔️ | ✔️ | ✔️
|
||||||
rotate-pdf | ✔️ | ✔️ | ✔️
|
pdf-to-single-page | ✔️ | ✔️ | ✔️
|
||||||
sanitize-pdf | ✔️ | ✔️ | ✔️
|
remove-pages | ✔️ | ✔️ | ✔️
|
||||||
scale-pages | ✔️ | ✔️ | ✔️
|
remove-password | ✔️ | ✔️ | ✔️
|
||||||
sign | ✔️ | ✔️ | ✔️
|
rotate-pdf | ✔️ | ✔️ | ✔️
|
||||||
show-javascript | ✔️ | ✔️ | ✔️
|
sanitize-pdf | ✔️ | ✔️ | ✔️
|
||||||
split-pdfs | ✔️ | ✔️ | ✔️
|
scale-pages | ✔️ | ✔️ | ✔️
|
||||||
file-to-pdf | | ✔️ | ✔️
|
sign | ✔️ | ✔️ | ✔️
|
||||||
pdf-to-html | | ✔️ | ✔️
|
show-javascript | ✔️ | ✔️ | ✔️
|
||||||
pdf-to-presentation | | ✔️ | ✔️
|
split-by-size-or-count | ✔️ | ✔️ | ✔️
|
||||||
pdf-to-text | | ✔️ | ✔️
|
split-pdf-by-sections | ✔️ | ✔️ | ✔️
|
||||||
pdf-to-word | | ✔️ | ✔️
|
split-pdfs | ✔️ | ✔️ | ✔️
|
||||||
pdf-to-xml | | ✔️ | ✔️
|
file-to-pdf | | ✔️ | ✔️
|
||||||
repair | | ✔️ | ✔️
|
pdf-to-html | | ✔️ | ✔️
|
||||||
xlsx-to-pdf | | ✔️ | ✔️
|
pdf-to-presentation | | ✔️ | ✔️
|
||||||
compress-pdf | | | ✔️
|
pdf-to-text | | ✔️ | ✔️
|
||||||
extract-image-scans | | | ✔️
|
pdf-to-word | | ✔️ | ✔️
|
||||||
ocr-pdf | | | ✔️
|
pdf-to-xml | | ✔️ | ✔️
|
||||||
pdf-to-pdfa | | | ✔️
|
repair | | ✔️ | ✔️
|
||||||
remove-blanks | | | ✔️
|
xlsx-to-pdf | | ✔️ | ✔️
|
||||||
|
compress-pdf | | | ✔️
|
||||||
|
extract-image-scans | | | ✔️
|
||||||
|
ocr-pdf | | | ✔️
|
||||||
|
pdf-to-pdfa | | | ✔️
|
||||||
|
remove-blanks | | | ✔️
|
||||||
|
|||||||
165
build.gradle
165
build.gradle
@@ -1,18 +1,28 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id 'java'
|
id 'java'
|
||||||
id 'org.springframework.boot' version '3.1.2'
|
id 'org.springframework.boot' version '3.2.2'
|
||||||
id 'io.spring.dependency-management' version '1.1.2'
|
id 'io.spring.dependency-management' version '1.1.3'
|
||||||
id 'org.springdoc.openapi-gradle-plugin' version '1.6.0'
|
id 'org.springdoc.openapi-gradle-plugin' version '1.8.0'
|
||||||
id "io.swagger.swaggerhub" version "1.2.0"
|
id "io.swagger.swaggerhub" version "1.3.2"
|
||||||
id 'edu.sc.seis.launch4j' version '3.0.3'
|
id 'edu.sc.seis.launch4j' version '3.0.5'
|
||||||
|
id 'com.diffplug.spotless' version '6.25.0'
|
||||||
|
id 'com.github.jk1.dependency-license-report' version '2.5'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
import com.github.jk1.license.render.*
|
||||||
|
|
||||||
group = 'stirling.software'
|
group = 'stirling.software'
|
||||||
version = '0.14.2'
|
version = '0.21.0'
|
||||||
sourceCompatibility = '17'
|
sourceCompatibility = '17'
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
licenseReport {
|
||||||
|
renderers = [new JsonReportRenderer()]
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
@@ -32,7 +42,6 @@ sourceSets {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
openApi {
|
openApi {
|
||||||
apiDocsUrl = "http://localhost:8080/v1/api-docs"
|
apiDocsUrl = "http://localhost:8080/v1/api-docs"
|
||||||
outputDir = file("$projectDir")
|
outputDir = file("$projectDir")
|
||||||
@@ -46,15 +55,15 @@ launch4j {
|
|||||||
outfile="Stirling-PDF.exe"
|
outfile="Stirling-PDF.exe"
|
||||||
headerType="console"
|
headerType="console"
|
||||||
jarTask = tasks.bootJar
|
jarTask = tasks.bootJar
|
||||||
|
|
||||||
errTitle="Encountered error, Do you have Java 17?"
|
errTitle="Encountered error, Do you have Java 17?"
|
||||||
downloadUrl="https://download.oracle.com/java/17/latest/jdk-17_windows-x64_bin.exe"
|
downloadUrl="https://download.oracle.com/java/17/latest/jdk-17_windows-x64_bin.exe"
|
||||||
variables=["BROWSER_OPEN=true"]
|
variables=["BROWSER_OPEN=true", "ENDPOINTS_GROUPS_TO_REMOVE=CLI"]
|
||||||
jreMinVersion="17"
|
jreMinVersion="17"
|
||||||
|
|
||||||
mutexName="Stirling-PDF"
|
mutexName="Stirling-PDF"
|
||||||
windowTitle="Stirling-PDF"
|
windowTitle="Stirling-PDF"
|
||||||
|
|
||||||
messagesStartupError="An error occurred while starting Stirling-PDF"
|
messagesStartupError="An error occurred while starting Stirling-PDF"
|
||||||
//messagesJreNotFoundError="This application requires a Java Runtime Environment, Please download Java 17."
|
//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."
|
messagesJreVersionError="You are running the wrong version of Java, Please download Java 17."
|
||||||
@@ -62,45 +71,103 @@ launch4j {
|
|||||||
messagesInstanceAlreadyExists="Stirling-PDF is already running."
|
messagesInstanceAlreadyExists="Stirling-PDF is already running."
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
spotless {
|
||||||
implementation 'org.yaml:snakeyaml:2.1'
|
java {
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-web:3.1.2'
|
target project.fileTree('src/main/java')
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf:3.1.2'
|
|
||||||
|
googleJavaFormat('1.19.1').aosp().reorderImports(false)
|
||||||
if (System.getenv('DOCKER_ENABLE_SECURITY') != 'false') {
|
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-security:3.1.2'
|
importOrder('java', 'javax', 'org', 'com', 'net', 'io')
|
||||||
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5:3.1.2.RELEASE'
|
toggleOffOn()
|
||||||
implementation "org.springframework.boot:spring-boot-starter-data-jpa"
|
trimTrailingWhitespace()
|
||||||
implementation "com.h2database:h2"
|
indentWithSpaces()
|
||||||
|
endWithNewline()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
testImplementation 'org.springframework.boot:spring-boot-starter-test:3.1.2'
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// https://mvnrepository.com/artifact/org.apache.pdfbox/jbig2-imageio
|
dependencies {
|
||||||
implementation group: 'org.apache.pdfbox', name: 'jbig2-imageio', version: '3.0.4'
|
//security updates
|
||||||
implementation 'commons-io:commons-io:2.13.0'
|
implementation 'ch.qos.logback:logback-classic:1.4.14'
|
||||||
|
implementation 'ch.qos.logback:logback-core:1.4.14'
|
||||||
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.1.0'
|
implementation 'org.springframework:spring-webmvc:6.1.2'
|
||||||
|
|
||||||
|
implementation("io.github.pixee:java-security-toolkit:1.1.2")
|
||||||
|
|
||||||
//general PDF
|
implementation 'org.yaml:snakeyaml:2.2'
|
||||||
implementation 'org.apache.pdfbox:pdfbox:2.0.29'
|
implementation 'org.springframework.boot:spring-boot-starter-web:3.2.2'
|
||||||
implementation 'org.apache.pdfbox:xmpbox:2.0.29'
|
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf:3.2.2'
|
||||||
implementation 'org.bouncycastle:bcprov-jdk15on:1.70'
|
|
||||||
implementation 'org.bouncycastle:bcpkix-jdk15on:1.70'
|
if (System.getenv('DOCKER_ENABLE_SECURITY') != 'false') {
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-actuator'
|
implementation 'org.springframework.boot:spring-boot-starter-security:3.2.2'
|
||||||
implementation 'io.micrometer:micrometer-core'
|
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5:3.1.2.RELEASE'
|
||||||
implementation group: 'com.google.zxing', name: 'core', version: '3.5.1'
|
implementation "org.springframework.boot:spring-boot-starter-data-jpa:3.2.2"
|
||||||
|
|
||||||
|
//2.2.x requires rebuild of DB file.. need migration path
|
||||||
|
implementation "com.h2database:h2:2.1.214"
|
||||||
|
}
|
||||||
|
|
||||||
|
testImplementation 'org.springframework.boot:spring-boot-starter-test:3.2.2'
|
||||||
|
|
||||||
|
// 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.9') {
|
||||||
|
exclude group: 'commons-logging', module: 'commons-logging'
|
||||||
|
}
|
||||||
|
|
||||||
|
implementation ('org.apache.pdfbox:pdfbox:3.0.1'){
|
||||||
|
exclude group: 'commons-logging', module: 'commons-logging'
|
||||||
|
}
|
||||||
|
|
||||||
|
implementation ('org.apache.pdfbox:xmpbox:3.0.1'){
|
||||||
|
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:3.2.2'
|
||||||
|
implementation 'io.micrometer:micrometer-core:1.12.2'
|
||||||
|
implementation group: 'com.google.zxing', name: 'core', version: '3.5.2'
|
||||||
// https://mvnrepository.com/artifact/org.commonmark/commonmark
|
// https://mvnrepository.com/artifact/org.commonmark/commonmark
|
||||||
implementation 'org.commonmark:commonmark:0.21.0'
|
implementation 'org.commonmark:commonmark:0.21.0'
|
||||||
|
implementation 'org.commonmark:commonmark-ext-gfm-tables:0.21.0'
|
||||||
// https://mvnrepository.com/artifact/com.github.vladimir-bukhtoyarov/bucket4j-core
|
// https://mvnrepository.com/artifact/com.github.vladimir-bukhtoyarov/bucket4j-core
|
||||||
implementation 'com.github.vladimir-bukhtoyarov:bucket4j-core:7.6.0'
|
implementation 'com.github.vladimir-bukhtoyarov:bucket4j-core:7.6.0'
|
||||||
|
|
||||||
developmentOnly("org.springframework.boot:spring-boot-devtools")
|
|
||||||
compileOnly 'org.projectlombok:lombok:1.18.28'
|
|
||||||
annotationProcessor 'org.projectlombok:lombok:1.18.28'
|
|
||||||
|
|
||||||
|
developmentOnly("org.springframework.boot:spring-boot-devtools:3.2.2")
|
||||||
|
compileOnly 'org.projectlombok:lombok:1.18.30'
|
||||||
|
annotationProcessor 'org.projectlombok:lombok:1.18.28'
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.withType(JavaCompile) {
|
||||||
|
dependsOn 'spotlessApply'
|
||||||
|
}
|
||||||
|
compileJava {
|
||||||
|
options.compilerArgs << '-parameters'
|
||||||
}
|
}
|
||||||
|
|
||||||
task writeVersion {
|
task writeVersion {
|
||||||
@@ -128,11 +195,11 @@ jar {
|
|||||||
attributes 'Implementation-Title': 'Stirling-PDF',
|
attributes 'Implementation-Title': 'Stirling-PDF',
|
||||||
'Implementation-Version': project.version
|
'Implementation-Version': project.version
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.named('test') {
|
tasks.named('test') {
|
||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
}
|
}
|
||||||
|
|
||||||
task printVersion {
|
task printVersion {
|
||||||
|
|||||||
15
chart/stirling-pdf/Chart.yaml
Normal file
15
chart/stirling-pdf/Chart.yaml
Normal 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/Stirling-Tools/Stirling-PDF
|
||||||
|
keywords:
|
||||||
|
- stirling-pdf
|
||||||
|
- helm
|
||||||
|
- charts repo
|
||||||
|
maintainers:
|
||||||
|
- name: Stirling-Tools
|
||||||
|
url: https://github.com/Stirling-Tools/Stirling-PDF
|
||||||
|
name: stirling-pdf-chart
|
||||||
|
sources:
|
||||||
|
- https://github.com/Stirling-Tools/Stirling-PDF
|
||||||
|
version: 1.0.0
|
||||||
30
chart/stirling-pdf/templates/NOTES.txt
Normal file
30
chart/stirling-pdf/templates/NOTES.txt
Normal 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 }}
|
||||||
129
chart/stirling-pdf/templates/_helpers.tpl
Normal file
129
chart/stirling-pdf/templates/_helpers.tpl
Normal 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 -}}
|
||||||
129
chart/stirling-pdf/templates/deployment.yaml
Normal file
129
chart/stirling-pdf/templates/deployment.yaml
Normal 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 }}
|
||||||
85
chart/stirling-pdf/templates/ingress.yaml
Normal file
85
chart/stirling-pdf/templates/ingress.yaml
Normal 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 -}}
|
||||||
16
chart/stirling-pdf/templates/pv.yaml
Normal file
16
chart/stirling-pdf/templates/pv.yaml
Normal 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 }}
|
||||||
27
chart/stirling-pdf/templates/pvc.yaml
Normal file
27
chart/stirling-pdf/templates/pvc.yaml
Normal 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 }}
|
||||||
48
chart/stirling-pdf/templates/service.yaml
Normal file
48
chart/stirling-pdf/templates/service.yaml
Normal 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 }}
|
||||||
13
chart/stirling-pdf/templates/serviceaccount.yaml
Normal file
13
chart/stirling-pdf/templates/serviceaccount.yaml
Normal 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 }}
|
||||||
31
chart/stirling-pdf/templates/servicemonitor.yaml
Normal file
31
chart/stirling-pdf/templates/servicemonitor.yaml
Normal 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 }}
|
||||||
239
chart/stirling-pdf/values.yaml
Normal file
239
chart/stirling-pdf/values.yaml
Normal 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:
|
||||||
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 8.7 KiB |
@@ -1,310 +1,110 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
|
||||||
<svg
|
<svg
|
||||||
width="99.537987mm"
|
|
||||||
height="95.209366mm"
|
|
||||||
viewBox="0 0 99.537987 95.209366"
|
|
||||||
version="1.1"
|
version="1.1"
|
||||||
id="svg745"
|
id="Layer_1"
|
||||||
|
x="0px"
|
||||||
|
y="0px"
|
||||||
|
viewBox="0 0 512 512"
|
||||||
|
style="enable-background:new 0 0 512 512;"
|
||||||
xml:space="preserve"
|
xml:space="preserve"
|
||||||
inkscape:version="1.2.1 (9c6d41e4, 2022-07-14)"
|
sodipodi:docname="favicon.svg"
|
||||||
sodipodi:docname="stirling.svg"
|
inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
|
||||||
inkscape:export-filename="stirling.png"
|
inkscape:export-filename="favicon.png"
|
||||||
inkscape:export-xdpi="80"
|
inkscape:export-xdpi="96"
|
||||||
inkscape:export-ydpi="80"
|
inkscape:export-ydpi="96"
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
|
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||||
id="namedview747"
|
id="defs173">
|
||||||
pagecolor="#ffffff"
|
|
||||||
bordercolor="#666666"
|
|
||||||
borderopacity="1.0"
|
<linearGradient
|
||||||
inkscape:showpageshadow="2"
|
id="XMLID_5_"
|
||||||
inkscape:pageopacity="0.0"
|
gradientUnits="userSpaceOnUse"
|
||||||
inkscape:pagecheckerboard="0"
|
x1="304.496"
|
||||||
inkscape:deskcolor="#d1d1d1"
|
y1="422.9102"
|
||||||
inkscape:document-units="mm"
|
x2="316.036"
|
||||||
showgrid="false"
|
y2="326.2626">
|
||||||
inkscape:zoom="0.914906"
|
<stop
|
||||||
inkscape:cx="184.17193"
|
offset="0"
|
||||||
inkscape:cy="509.88845"
|
style="stop-color:#DCF1F3"
|
||||||
inkscape:window-width="2293"
|
id="stop156" />
|
||||||
inkscape:window-height="1387"
|
<stop
|
||||||
inkscape:window-x="122"
|
offset="1"
|
||||||
inkscape:window-y="25"
|
style="stop-color:#C2C2C9"
|
||||||
inkscape:window-maximized="0"
|
id="stop158" />
|
||||||
inkscape:current-layer="svg745" /><defs
|
</linearGradient>
|
||||||
id="defs742"><linearGradient
|
|
||||||
inkscape:collect="always"
|
</defs><sodipodi:namedview
|
||||||
id="linearGradient72198"><stop
|
id="namedview171"
|
||||||
style="stop-color:#ccd6d7;stop-opacity:1;"
|
pagecolor="#ffffff"
|
||||||
offset="0"
|
bordercolor="#000000"
|
||||||
id="stop72194" /><stop
|
borderopacity="0.25"
|
||||||
style="stop-color:#0f3a3f;stop-opacity:1;"
|
inkscape:showpageshadow="2"
|
||||||
offset="1"
|
inkscape:pageopacity="0.0"
|
||||||
id="stop72196" /></linearGradient><linearGradient
|
inkscape:pagecheckerboard="0"
|
||||||
inkscape:collect="always"
|
inkscape:deskcolor="#d1d1d1"
|
||||||
id="linearGradient71928"><stop
|
showgrid="false"
|
||||||
style="stop-color:#4b0005;stop-opacity:1;"
|
inkscape:zoom="1.4142136"
|
||||||
offset="0"
|
inkscape:cx="219.91021"
|
||||||
id="stop71924" /><stop
|
inkscape:cy="232.63813"
|
||||||
style="stop-color:#8f000c;stop-opacity:1;"
|
inkscape:window-width="3840"
|
||||||
offset="1"
|
inkscape:window-height="2054"
|
||||||
id="stop71926" /></linearGradient><linearGradient
|
inkscape:window-x="2869"
|
||||||
inkscape:collect="always"
|
inkscape:window-y="-11"
|
||||||
id="linearGradient71920"><stop
|
inkscape:window-maximized="1"
|
||||||
style="stop-color:#4b0005;stop-opacity:1;"
|
inkscape:current-layer="XMLID_4_" />
|
||||||
offset="0"
|
<style
|
||||||
id="stop71916" /><stop
|
type="text/css"
|
||||||
style="stop-color:#8f000c;stop-opacity:1;"
|
id="style150">
|
||||||
offset="1"
|
.st0{fill:#FFFFFF;}
|
||||||
id="stop71918" /></linearGradient><linearGradient
|
.st1{fill:#C02223;}
|
||||||
inkscape:collect="always"
|
.st2{fill:#882425;}
|
||||||
id="linearGradient69598"><stop
|
.st3{fill:url(#XMLID_5_);}
|
||||||
style="stop-color:#6a0007;stop-opacity:1;"
|
.st4{fill:url(#XMLID_7_);}
|
||||||
offset="0"
|
</style>
|
||||||
id="stop69594" /><stop
|
|
||||||
style="stop-color:#b8000f;stop-opacity:1;"
|
<g
|
||||||
offset="1"
|
id="XMLID_4_">
|
||||||
id="stop69596" /></linearGradient><linearGradient
|
<path
|
||||||
inkscape:collect="always"
|
id="XMLID_131_"
|
||||||
id="linearGradient46361"><stop
|
class="st1"
|
||||||
style="stop-color:#f7f6f8;stop-opacity:1;"
|
d="M 347.01402,14.355825 98.978019,69.02261 C 73.825483,74.547445 55.942464,96.792175 55.942464,122.52628 v 315.06096 c 0,22.39012 16.719895,41.14548 38.819234,43.76251 L 224.8861,498.36042 339.48636,384.26465 455.76603,265.15425 453.73057,84.870162 C 453.43979,62.916214 433.08513,46.632491 411.71274,51.284984 l -28.78729,6.251786 0.14539,-13.666697 C 383.36162,24.678542 365.62399,10.284894 347.01402,14.355825 Z"
|
||||||
offset="0"
|
sodipodi:nodetypes="ccssccccccccc"
|
||||||
id="stop46359" /><stop
|
style="stroke-width:1.45391" /><path
|
||||||
style="stop-color:#b7b7b5;stop-opacity:1;"
|
id="XMLID_117_"
|
||||||
offset="1"
|
class="st2"
|
||||||
id="stop46357" /></linearGradient><linearGradient
|
d="m 383.21622,57.53677 v 285.8375 L 456.05681,265.00885 454.02135,78.763767 C 453.87595,59.863016 436.28372,45.905539 417.81914,49.97647 Z"
|
||||||
inkscape:collect="always"
|
style="stroke-width:1.45391" /><polygon
|
||||||
id="linearGradient40554"><stop
|
id="XMLID_18_"
|
||||||
style="stop-color:#f4f2f4;stop-opacity:1;"
|
class="st3"
|
||||||
offset="0"
|
points="234.7,422.6 368.5,387.7 393.5,262.2 "
|
||||||
id="stop40550" /><stop
|
style="fill:url(#XMLID_5_)"
|
||||||
style="stop-color:#57767b;stop-opacity:1;"
|
transform="matrix(1.4556308,0,0,1.4548265,-116.73161,-116.45231)" />
|
||||||
offset="1"
|
<linearGradient
|
||||||
id="stop40552" /></linearGradient><linearGradient
|
id="XMLID_7_"
|
||||||
inkscape:collect="always"
|
gradientUnits="userSpaceOnUse"
|
||||||
id="linearGradient39095"><stop
|
x1="223.0838"
|
||||||
style="stop-color:#285459;stop-opacity:1;"
|
y1="372.7559"
|
||||||
offset="0"
|
x2="241.4174"
|
||||||
id="stop39093" /><stop
|
y2="114.557"
|
||||||
style="stop-color:#a6b6af;stop-opacity:1;"
|
gradientTransform="matrix(1.4539039,0,0,1.4539039,-116.19976,-116.20474)">
|
||||||
offset="1"
|
<stop
|
||||||
id="stop39091" /></linearGradient><linearGradient
|
offset="0"
|
||||||
inkscape:collect="always"
|
style="stop-color:#DCF1F3"
|
||||||
id="linearGradient36672"><stop
|
id="stop163" />
|
||||||
style="stop-color:#da453f;stop-opacity:1;"
|
<stop
|
||||||
offset="0"
|
offset="1"
|
||||||
id="stop36668" /><stop
|
style="stop-color:#C2C2C9"
|
||||||
style="stop-color:#a60008;stop-opacity:1;"
|
id="stop165" />
|
||||||
offset="1"
|
</linearGradient>
|
||||||
id="stop36670" /></linearGradient><linearGradient
|
<path
|
||||||
inkscape:collect="always"
|
id="XMLID_6_"
|
||||||
id="linearGradient19524"><stop
|
class="st4"
|
||||||
style="stop-color:#3a4b4f;stop-opacity:1;"
|
d="m 282.89686,214.84917 c 0,0 -22.24473,-28.93269 -38.67384,-36.78377 -10.46811,-4.94327 -26.02489,-6.83335 -38.23768,-0.72695 -18.02841,9.0142 -19.91848,34.31213 -3.34397,44.34406 3.92553,2.47165 9.15959,4.50711 15.99294,6.10641 36.63838,8.43264 97.12077,25.87949 89.70587,96.10304 0,0 -4.21633,65.86185 -73.56753,73.42215 -12.2128,1.30851 -24.57098,0.43617 -36.493,-2.32625 -16.42911,-3.63476 -45.50719,-11.04967 -59.75545,-19.91849 l -2.61703,-75.16682 h 6.97875 c 0,0 13.81208,33.43978 53.06749,49.57812 7.26952,2.90781 15.26599,4.07093 22.97168,2.90781 9.74116,-1.45391 21.22699,-6.68796 25.87949,-22.53551 0,0 7.85108,-23.11707 -32.85823,-35.76604 -32.56744,-10.17733 -63.24481,-20.64543 -75.89378,-54.95757 -5.961,-16.28371 -6.97874,-34.31212 -2.90781,-51.61358 5.37944,-22.53551 20.79082,-54.23062 64.40794,-67.89732 0,0 57.28381,-15.55677 96.53922,5.52484 l -1.74468,89.70587 z"
|
||||||
offset="0"
|
style="fill:url(#XMLID_7_);stroke-width:1.45391" />
|
||||||
id="stop19522" /><stop
|
</g>
|
||||||
style="stop-color:#617979;stop-opacity:0.97461927;"
|
</svg>
|
||||||
offset="1"
|
|
||||||
id="stop19520" /></linearGradient><linearGradient
|
|
||||||
inkscape:collect="always"
|
|
||||||
id="linearGradient17996"><stop
|
|
||||||
style="stop-color:#401016;stop-opacity:1;"
|
|
||||||
offset="0"
|
|
||||||
id="stop17994" /><stop
|
|
||||||
style="stop-color:#761f19;stop-opacity:1;"
|
|
||||||
offset="1"
|
|
||||||
id="stop17992" /></linearGradient><linearGradient
|
|
||||||
inkscape:collect="always"
|
|
||||||
id="linearGradient15569"><stop
|
|
||||||
style="stop-color:#8ea8ad;stop-opacity:1;"
|
|
||||||
offset="0"
|
|
||||||
id="stop15565" /><stop
|
|
||||||
style="stop-color:#e9e7eb;stop-opacity:1;"
|
|
||||||
offset="1"
|
|
||||||
id="stop15567" /></linearGradient><linearGradient
|
|
||||||
inkscape:collect="always"
|
|
||||||
id="linearGradient15557"><stop
|
|
||||||
style="stop-color:#9b0e11;stop-opacity:1;"
|
|
||||||
offset="0"
|
|
||||||
id="stop15553" /><stop
|
|
||||||
style="stop-color:#370707;stop-opacity:1;"
|
|
||||||
offset="1"
|
|
||||||
id="stop15555" /></linearGradient><linearGradient
|
|
||||||
inkscape:collect="always"
|
|
||||||
xlink:href="#linearGradient15557"
|
|
||||||
id="linearGradient15559"
|
|
||||||
x1="120.06672"
|
|
||||||
y1="63.25761"
|
|
||||||
x2="135.16347"
|
|
||||||
y2="78.078682"
|
|
||||||
gradientUnits="userSpaceOnUse" /><linearGradient
|
|
||||||
inkscape:collect="always"
|
|
||||||
xlink:href="#linearGradient15569"
|
|
||||||
id="linearGradient15571"
|
|
||||||
x1="127.97037"
|
|
||||||
y1="101.66144"
|
|
||||||
x2="133.88971"
|
|
||||||
y2="104.77026"
|
|
||||||
gradientUnits="userSpaceOnUse" /><linearGradient
|
|
||||||
inkscape:collect="always"
|
|
||||||
xlink:href="#linearGradient17996"
|
|
||||||
id="linearGradient17998"
|
|
||||||
x1="117.9284"
|
|
||||||
y1="86.055084"
|
|
||||||
x2="130.67392"
|
|
||||||
y2="76.945541"
|
|
||||||
gradientUnits="userSpaceOnUse" /><linearGradient
|
|
||||||
inkscape:collect="always"
|
|
||||||
xlink:href="#linearGradient19524"
|
|
||||||
id="linearGradient19528"
|
|
||||||
x1="130.98172"
|
|
||||||
y1="82.402977"
|
|
||||||
x2="135.72115"
|
|
||||||
y2="86.45166"
|
|
||||||
gradientUnits="userSpaceOnUse" /><linearGradient
|
|
||||||
inkscape:collect="always"
|
|
||||||
xlink:href="#linearGradient36672"
|
|
||||||
id="linearGradient36674"
|
|
||||||
x1="63.797714"
|
|
||||||
y1="74.81752"
|
|
||||||
x2="96.636673"
|
|
||||||
y2="120.29293"
|
|
||||||
gradientUnits="userSpaceOnUse" /><linearGradient
|
|
||||||
inkscape:collect="always"
|
|
||||||
xlink:href="#linearGradient39095"
|
|
||||||
id="linearGradient39097"
|
|
||||||
x1="120.54506"
|
|
||||||
y1="124.76902"
|
|
||||||
x2="128.04152"
|
|
||||||
y2="126.0704"
|
|
||||||
gradientUnits="userSpaceOnUse" /><linearGradient
|
|
||||||
inkscape:collect="always"
|
|
||||||
xlink:href="#linearGradient40554"
|
|
||||||
id="linearGradient40556"
|
|
||||||
x1="113.84585"
|
|
||||||
y1="114.04285"
|
|
||||||
x2="119.65858"
|
|
||||||
y2="128.50244"
|
|
||||||
gradientUnits="userSpaceOnUse" /><linearGradient
|
|
||||||
inkscape:collect="always"
|
|
||||||
xlink:href="#linearGradient46361"
|
|
||||||
id="linearGradient46363"
|
|
||||||
x1="73.993439"
|
|
||||||
y1="114.13906"
|
|
||||||
x2="94.845322"
|
|
||||||
y2="71.247383"
|
|
||||||
gradientUnits="userSpaceOnUse" /><linearGradient
|
|
||||||
inkscape:collect="always"
|
|
||||||
xlink:href="#linearGradient69598"
|
|
||||||
id="linearGradient69600"
|
|
||||||
x1="95.854446"
|
|
||||||
y1="114.66749"
|
|
||||||
x2="103.77842"
|
|
||||||
y2="120.1887"
|
|
||||||
gradientUnits="userSpaceOnUse" /><linearGradient
|
|
||||||
inkscape:collect="always"
|
|
||||||
xlink:href="#linearGradient71920"
|
|
||||||
id="linearGradient71922"
|
|
||||||
x1="98.580376"
|
|
||||||
y1="87.186958"
|
|
||||||
x2="118.09738"
|
|
||||||
y2="101.19449"
|
|
||||||
gradientUnits="userSpaceOnUse" /><linearGradient
|
|
||||||
inkscape:collect="always"
|
|
||||||
xlink:href="#linearGradient71928"
|
|
||||||
id="linearGradient71930"
|
|
||||||
x1="78.278267"
|
|
||||||
y1="97.433273"
|
|
||||||
x2="92.313202"
|
|
||||||
y2="104.33479"
|
|
||||||
gradientUnits="userSpaceOnUse" /><linearGradient
|
|
||||||
inkscape:collect="always"
|
|
||||||
xlink:href="#linearGradient72198"
|
|
||||||
id="linearGradient72200"
|
|
||||||
x1="125.76636"
|
|
||||||
y1="138.46817"
|
|
||||||
x2="123.3327"
|
|
||||||
y2="126.03291"
|
|
||||||
gradientUnits="userSpaceOnUse" /></defs><g
|
|
||||||
inkscape:groupmode="layer"
|
|
||||||
id="layer4"
|
|
||||||
inkscape:label="background"
|
|
||||||
style="display:inline"
|
|
||||||
sodipodi:insensitive="true"
|
|
||||||
transform="translate(-51.420144,-44.470286)"><rect
|
|
||||||
style="display:inline;fill:#ccd6d7;fill-opacity:1;stroke:none;stroke-width:4.13755;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:3.2"
|
|
||||||
id="rect72067"
|
|
||||||
width="99.481979"
|
|
||||||
height="94.999512"
|
|
||||||
x="51.476147"
|
|
||||||
y="44.680138" /></g><g
|
|
||||||
inkscape:groupmode="layer"
|
|
||||||
id="layer5"
|
|
||||||
inkscape:label="shadow"
|
|
||||||
style="display:inline"
|
|
||||||
sodipodi:insensitive="true"
|
|
||||||
transform="translate(-51.420144,-44.470286)"><path
|
|
||||||
style="display:inline;fill:url(#linearGradient72200);fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
|
||||||
d="m 84.146049,134.73858 c 0,0 11.721038,2.48294 17.938661,2.91673 6.21763,0.43378 14.75251,0.59994 22.41237,-0.43379 8.01008,-1.081 13.19907,-2.22733 14.50043,-2.66112 1.30136,-0.43379 4.00784,-1.24297 4.15244,-2.25514 0.1446,-1.01217 -3.4703,-2.74733 -6.21763,-3.32571 -2.74732,-0.57838 -12.72444,-1.44596 -14.89337,-1.44596 -2.16894,0 -37.892901,7.20499 -37.892901,7.20499 z"
|
|
||||||
id="path72192"
|
|
||||||
sodipodi:nodetypes="cssssssc" /></g><g
|
|
||||||
inkscape:groupmode="layer"
|
|
||||||
id="layer2"
|
|
||||||
inkscape:label="Origami"
|
|
||||||
style="display:inline"
|
|
||||||
sodipodi:insensitive="true"
|
|
||||||
transform="translate(-51.420144,-44.470286)"><path
|
|
||||||
style="display:inline;fill:url(#linearGradient40556);fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
|
||||||
d="m 84.460552,134.86721 35.165798,-6.85679 16.15467,-42.7383 z"
|
|
||||||
id="path960"
|
|
||||||
sodipodi:nodetypes="cccc" /><path
|
|
||||||
style="fill:url(#linearGradient15571);fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
|
||||||
d="m 135.71493,85.428056 0.3984,45.049024 -9.66213,-20.46173 z"
|
|
||||||
id="path964"
|
|
||||||
sodipodi:nodetypes="cccc" /><path
|
|
||||||
style="display:inline;fill:url(#linearGradient39097);fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
|
||||||
d="m 119.60518,128.00293 16.5337,2.48693 -9.68769,-20.5512 z"
|
|
||||||
id="path966"
|
|
||||||
sodipodi:nodetypes="cccc" /><path
|
|
||||||
style="display:inline;fill:url(#linearGradient15559);fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
|
||||||
d="m 118.42209,57.022622 12.70423,-2.766809 -0.0785,25.087175 -12.43878,-13.376518 z"
|
|
||||||
id="path968"
|
|
||||||
sodipodi:nodetypes="ccccc" /><path
|
|
||||||
style="display:inline;fill:url(#linearGradient19528);fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
|
||||||
d="m 135.72114,85.386768 -4.84219,-6.459493 0.0305,11.126604 z"
|
|
||||||
id="path970"
|
|
||||||
sodipodi:nodetypes="cccc" /><path
|
|
||||||
style="display:inline;fill:url(#linearGradient17998);fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
|
||||||
d="m 119.10273,65.682415 11.96883,13.44935 -0.0899,10.819868 -11.88577,11.430427 z"
|
|
||||||
id="path972"
|
|
||||||
sodipodi:nodetypes="ccccc" /><path
|
|
||||||
style="display:inline;fill:url(#linearGradient36674);fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
|
||||||
d="M 62.145635,130.15618 62.043392,65.435258 c 0,0 0.179321,-2.778132 1.89516,-4.036097 1.874923,-1.374597 4.341768,-1.894096 4.341768,-1.894096 l 50.91788,-11.349167 -0.0113,53.144272 -34.733274,33.56547 z"
|
|
||||||
id="path958"
|
|
||||||
sodipodi:nodetypes="ccsccccc" /></g><g
|
|
||||||
inkscape:groupmode="layer"
|
|
||||||
id="layer3"
|
|
||||||
inkscape:label="Letter"
|
|
||||||
style="display:inline"
|
|
||||||
sodipodi:insensitive="true"
|
|
||||||
transform="translate(-51.420144,-44.470286)"><path
|
|
||||||
style="display:inline;fill:url(#linearGradient69600);fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
|
||||||
d="m 94.780881,91.406803 16.870379,17.074877 -23.723345,23.00249 -18.122131,-17.99816 5.497473,-2.80607 18.404054,-0.0511 2.35163,-8.23071 z"
|
|
||||||
id="path54894"
|
|
||||||
sodipodi:nodetypes="cccccccc" /><path
|
|
||||||
style="display:inline;fill:url(#linearGradient71930);fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
|
||||||
d="m 72.440405,92.224764 16.15467,15.745686 4.089788,-6.79927 z"
|
|
||||||
id="path54892" /><path
|
|
||||||
style="display:inline;fill:url(#linearGradient71922);fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
|
||||||
d="m 95.138739,84.965385 1.124691,-14.109776 22.92453,22.286787 0.008,8.164604 -3.28863,3.16649 z"
|
|
||||||
id="path54890"
|
|
||||||
sodipodi:nodetypes="cccccc"
|
|
||||||
inkscape:label="path54890" /><path
|
|
||||||
style="display:inline;fill:url(#linearGradient46363);fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
|
||||||
d="m 95.138739,84.965385 h 1.226936 l -0.05112,-14.109776 c 0,0 -5.776827,-3.220709 -12.167126,-2.40275 -6.390296,0.817957 -8.151582,2.1248 -10.58233,4.396523 -1.90229,1.777838 -2.913974,3.527446 -3.987546,7.157132 -0.512646,1.733226 -0.281963,5.988892 0.613471,8.537436 0.664591,1.891528 2.453873,4.294281 4.958868,6.134686 2.662335,1.956002 8.281825,3.527443 8.281825,3.527443 0,0 5.134614,1.887351 5.572338,4.294281 0.308137,1.69437 -0.102243,3.22071 -1.635914,4.95887 -1.258314,1.42609 -3.62969,1.99377 -6.288054,1.07357 -2.658364,-0.92021 -6.139514,-3.85065 -7.106009,-4.90775 -0.817958,-0.89464 -2.820665,-3.06173 -2.890231,-4.294021 -0.01209,-0.214205 -1.229505,-0.0963 -1.229505,-0.0963 l -0.0723,14.256941 5.879073,2.24938 c 0,0 5.214483,1.78929 8.946415,1.43143 3.731934,-0.35786 7.617235,-0.51122 11.604778,-5.16336 3.987542,-4.65213 3.680812,-12.831715 2.147141,-15.899056 -1.533673,-3.067344 -3.561212,-6.138812 -10.480087,-8.281826 -5.776829,-1.789283 -7.872846,-3.01622 -8.128458,-4.396524 -0.255611,-1.380305 0.0091,-4.253646 2.760607,-5.214481 3.220711,-1.124693 5.623462,-0.05112 7.05489,1.12469 1.431425,1.175817 5.572339,5.623462 5.572339,5.623462 z"
|
|
||||||
id="path46355"
|
|
||||||
sodipodi:nodetypes="cccssssscssssscccssssssscc" /></g></svg>
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 4.0 KiB |
31
exampleYmlFiles/docker-compose-latest-lite-security.yml
Normal file
31
exampleYmlFiles/docker-compose-latest-lite-security.yml
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
version: '3.3'
|
||||||
|
services:
|
||||||
|
stirling-pdf:
|
||||||
|
container_name: Stirling-PDF-Lite-Security
|
||||||
|
image: frooodle/s-pdf:latest-lite
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 2G
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP' && curl -fL http://localhost:8080/ | grep -q 'Please sign in'"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 16
|
||||||
|
ports:
|
||||||
|
- 8080:8080
|
||||||
|
volumes:
|
||||||
|
- /stirling/latest/data:/usr/share/tesseract-ocr/5/tessdata:rw
|
||||||
|
- /stirling/latest/config:/configs:rw
|
||||||
|
- /stirling/latest/logs:/logs:rw
|
||||||
|
environment:
|
||||||
|
DOCKER_ENABLE_SECURITY: "true"
|
||||||
|
SECURITY_ENABLELOGIN: "true"
|
||||||
|
SYSTEM_DEFAULTLOCALE: en-US
|
||||||
|
UI_APPNAME: Stirling-PDF-Lite
|
||||||
|
UI_HOMEDESCRIPTION: Demo site for Stirling-PDF-Lite Latest with Security
|
||||||
|
UI_APPNAMENAVBAR: Stirling-PDF-Lite Latest
|
||||||
|
SYSTEM_MAXFILESIZE: "100"
|
||||||
|
METRICS_ENABLED: "true"
|
||||||
|
SYSTEM_GOOGLEVISIBILITY: "true"
|
||||||
|
restart: on-failure:5
|
||||||
30
exampleYmlFiles/docker-compose-latest-lite.yml
Normal file
30
exampleYmlFiles/docker-compose-latest-lite.yml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
version: '3.3'
|
||||||
|
services:
|
||||||
|
stirling-pdf:
|
||||||
|
container_name: Stirling-PDF-Lite
|
||||||
|
image: frooodle/s-pdf:latest-lite
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 2G
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP' && curl -fL http://localhost:8080/ | grep -qv 'Please sign in'"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 16
|
||||||
|
ports:
|
||||||
|
- 8080:8080
|
||||||
|
volumes:
|
||||||
|
- /stirling/latest/config:/configs:rw
|
||||||
|
- /stirling/latest/logs:/logs:rw
|
||||||
|
environment:
|
||||||
|
DOCKER_ENABLE_SECURITY: "false"
|
||||||
|
SECURITY_ENABLELOGIN: "false"
|
||||||
|
SYSTEM_DEFAULTLOCALE: en-US
|
||||||
|
UI_APPNAME: Stirling-PDF-Lite
|
||||||
|
UI_HOMEDESCRIPTION: Demo site for Stirling-PDF-Lite Latest
|
||||||
|
UI_APPNAMENAVBAR: Stirling-PDF-Lite Latest
|
||||||
|
SYSTEM_MAXFILESIZE: "100"
|
||||||
|
METRICS_ENABLED: "true"
|
||||||
|
SYSTEM_GOOGLEVISIBILITY: "true"
|
||||||
|
restart: on-failure:5
|
||||||
31
exampleYmlFiles/docker-compose-latest-security.yml
Normal file
31
exampleYmlFiles/docker-compose-latest-security.yml
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
version: '3.3'
|
||||||
|
services:
|
||||||
|
stirling-pdf:
|
||||||
|
container_name: Stirling-PDF-Security
|
||||||
|
image: frooodle/s-pdf:latest
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 4G
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP' && curl -fL http://localhost:8080/ | grep -q 'Please sign in'"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 16
|
||||||
|
ports:
|
||||||
|
- 8080:8080
|
||||||
|
volumes:
|
||||||
|
- /stirling/latest/data:/usr/share/tesseract-ocr/5/tessdata:rw
|
||||||
|
- /stirling/latest/config:/configs:rw
|
||||||
|
- /stirling/latest/logs:/logs:rw
|
||||||
|
environment:
|
||||||
|
DOCKER_ENABLE_SECURITY: "true"
|
||||||
|
SECURITY_ENABLELOGIN: "true"
|
||||||
|
SYSTEM_DEFAULTLOCALE: en-US
|
||||||
|
UI_APPNAME: Stirling-PDF
|
||||||
|
UI_HOMEDESCRIPTION: Demo site for Stirling-PDF Latest with Security
|
||||||
|
UI_APPNAMENAVBAR: Stirling-PDF Latest
|
||||||
|
SYSTEM_MAXFILESIZE: "100"
|
||||||
|
METRICS_ENABLED: "true"
|
||||||
|
SYSTEM_GOOGLEVISIBILITY: "true"
|
||||||
|
restart: on-failure:5
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
version: '3.3'
|
||||||
|
services:
|
||||||
|
stirling-pdf:
|
||||||
|
container_name: Stirling-PDF-Ultra-Lite-Security
|
||||||
|
image: frooodle/s-pdf:latest-ultra-lite
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 1G
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP' && curl -fL http://localhost:8080/ | grep -q 'Please sign in'"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 16
|
||||||
|
ports:
|
||||||
|
- 8080:8080
|
||||||
|
volumes:
|
||||||
|
- /stirling/latest/data:/usr/share/tesseract-ocr/5/tessdata:rw
|
||||||
|
- /stirling/latest/config:/configs:rw
|
||||||
|
- /stirling/latest/logs:/logs:rw
|
||||||
|
environment:
|
||||||
|
DOCKER_ENABLE_SECURITY: "true"
|
||||||
|
SECURITY_ENABLELOGIN: "true"
|
||||||
|
SYSTEM_DEFAULTLOCALE: en-US
|
||||||
|
UI_APPNAME: Stirling-PDF-Lite
|
||||||
|
UI_HOMEDESCRIPTION: Demo site for Stirling-PDF-Lite Latest with Security
|
||||||
|
UI_APPNAMENAVBAR: Stirling-PDF-Lite Latest
|
||||||
|
SYSTEM_MAXFILESIZE: "100"
|
||||||
|
METRICS_ENABLED: "true"
|
||||||
|
SYSTEM_GOOGLEVISIBILITY: "true"
|
||||||
|
restart: on-failure:5
|
||||||
30
exampleYmlFiles/docker-compose-latest-ultra-lite.yml
Normal file
30
exampleYmlFiles/docker-compose-latest-ultra-lite.yml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
version: '3.3'
|
||||||
|
services:
|
||||||
|
stirling-pdf:
|
||||||
|
container_name: Stirling-PDF-Ultra-Lite
|
||||||
|
image: frooodle/s-pdf:latest-ultra-lite
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 1G
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP' && curl -fL http://localhost:8080/ | grep -qv 'Please sign in'"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 16
|
||||||
|
ports:
|
||||||
|
- 8080:8080
|
||||||
|
volumes:
|
||||||
|
- /stirling/latest/config:/configs:rw
|
||||||
|
- /stirling/latest/logs:/logs:rw
|
||||||
|
environment:
|
||||||
|
DOCKER_ENABLE_SECURITY: "false"
|
||||||
|
SECURITY_ENABLELOGIN: "false"
|
||||||
|
SYSTEM_DEFAULTLOCALE: en-US
|
||||||
|
UI_APPNAME: Stirling-PDF-Ultra-lite
|
||||||
|
UI_HOMEDESCRIPTION: Demo site for Stirling-PDF-Ultra-lite Latest
|
||||||
|
UI_APPNAMENAVBAR: Stirling-PDF-Ultra-lite Latest
|
||||||
|
SYSTEM_MAXFILESIZE: "100"
|
||||||
|
METRICS_ENABLED: "true"
|
||||||
|
SYSTEM_GOOGLEVISIBILITY: "true"
|
||||||
|
restart: on-failure:5
|
||||||
31
exampleYmlFiles/docker-compose-latest.yml
Normal file
31
exampleYmlFiles/docker-compose-latest.yml
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
version: '3.3'
|
||||||
|
services:
|
||||||
|
stirling-pdf:
|
||||||
|
container_name: Stirling-PDF
|
||||||
|
image: frooodle/s-pdf:latest
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 4G
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP' && curl -fL http://localhost:8080/ | grep -qv 'Please sign in'"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 16
|
||||||
|
ports:
|
||||||
|
- 8080:8080
|
||||||
|
volumes:
|
||||||
|
- /stirling/latest/data:/usr/share/tesseract-ocr/5/tessdata:rw
|
||||||
|
- /stirling/latest/config:/configs:rw
|
||||||
|
- /stirling/latest/logs:/logs:rw
|
||||||
|
environment:
|
||||||
|
DOCKER_ENABLE_SECURITY: "false"
|
||||||
|
SECURITY_ENABLELOGIN: "false"
|
||||||
|
SYSTEM_DEFAULTLOCALE: en-US
|
||||||
|
UI_APPNAME: Stirling-PDF
|
||||||
|
UI_HOMEDESCRIPTION: Demo site for Stirling-PDF Latest
|
||||||
|
UI_APPNAMENAVBAR: Stirling-PDF Latest
|
||||||
|
SYSTEM_MAXFILESIZE: "100"
|
||||||
|
METRICS_ENABLED: "true"
|
||||||
|
SYSTEM_GOOGLEVISIBILITY: "true"
|
||||||
|
restart: on-failure:5
|
||||||
182
gradlew.bat
vendored
182
gradlew.bat
vendored
@@ -1,91 +1,91 @@
|
|||||||
@rem
|
@rem
|
||||||
@rem Copyright 2015 the original author or authors.
|
@rem Copyright 2015 the original author or authors.
|
||||||
@rem
|
@rem
|
||||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
@rem you may not use this file except in compliance with the License.
|
@rem you may not use this file except in compliance with the License.
|
||||||
@rem You may obtain a copy of the License at
|
@rem You may obtain a copy of the License at
|
||||||
@rem
|
@rem
|
||||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||||
@rem
|
@rem
|
||||||
@rem Unless required by applicable law or agreed to in writing, software
|
@rem Unless required by applicable law or agreed to in writing, software
|
||||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
@rem See the License for the specific language governing permissions and
|
@rem See the License for the specific language governing permissions and
|
||||||
@rem limitations under the License.
|
@rem limitations under the License.
|
||||||
@rem
|
@rem
|
||||||
|
|
||||||
@if "%DEBUG%"=="" @echo off
|
@if "%DEBUG%"=="" @echo off
|
||||||
@rem ##########################################################################
|
@rem ##########################################################################
|
||||||
@rem
|
@rem
|
||||||
@rem Gradle startup script for Windows
|
@rem Gradle startup script for Windows
|
||||||
@rem
|
@rem
|
||||||
@rem ##########################################################################
|
@rem ##########################################################################
|
||||||
|
|
||||||
@rem Set local scope for the variables with windows NT shell
|
@rem Set local scope for the variables with windows NT shell
|
||||||
if "%OS%"=="Windows_NT" setlocal
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
set DIRNAME=%~dp0
|
set DIRNAME=%~dp0
|
||||||
if "%DIRNAME%"=="" set DIRNAME=.
|
if "%DIRNAME%"=="" set DIRNAME=.
|
||||||
set APP_BASE_NAME=%~n0
|
set APP_BASE_NAME=%~n0
|
||||||
set APP_HOME=%DIRNAME%
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||||
|
|
||||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||||
|
|
||||||
@rem Find java.exe
|
@rem Find java.exe
|
||||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
set JAVA_EXE=java.exe
|
set JAVA_EXE=java.exe
|
||||||
%JAVA_EXE% -version >NUL 2>&1
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
if %ERRORLEVEL% equ 0 goto execute
|
if %ERRORLEVEL% equ 0 goto execute
|
||||||
|
|
||||||
echo.
|
echo.
|
||||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
echo.
|
echo.
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
echo location of your Java installation.
|
echo location of your Java installation.
|
||||||
|
|
||||||
goto fail
|
goto fail
|
||||||
|
|
||||||
:findJavaFromJavaHome
|
:findJavaFromJavaHome
|
||||||
set JAVA_HOME=%JAVA_HOME:"=%
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
if exist "%JAVA_EXE%" goto execute
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
echo.
|
echo.
|
||||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||||
echo.
|
echo.
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
echo location of your Java installation.
|
echo location of your Java installation.
|
||||||
|
|
||||||
goto fail
|
goto fail
|
||||||
|
|
||||||
:execute
|
:execute
|
||||||
@rem Setup the command line
|
@rem Setup the command line
|
||||||
|
|
||||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
@rem Execute Gradle
|
@rem Execute Gradle
|
||||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||||
|
|
||||||
:end
|
:end
|
||||||
@rem End local scope for the variables with windows NT shell
|
@rem End local scope for the variables with windows NT shell
|
||||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||||
|
|
||||||
:fail
|
:fail
|
||||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
rem the _cmd.exe /c_ return code!
|
rem the _cmd.exe /c_ return code!
|
||||||
set EXIT_CODE=%ERRORLEVEL%
|
set EXIT_CODE=%ERRORLEVEL%
|
||||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||||
exit /b %EXIT_CODE%
|
exit /b %EXIT_CODE%
|
||||||
|
|
||||||
:mainEnd
|
:mainEnd
|
||||||
if "%OS%"=="Windows_NT" endlocal
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
:omega
|
:omega
|
||||||
|
|||||||
39
pipeline/defaultWebUIConfigs/Prepare-pdfs-for-email.json
Normal file
39
pipeline/defaultWebUIConfigs/Prepare-pdfs-for-email.json
Normal 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}"
|
||||||
|
}
|
||||||
33
pipeline/defaultWebUIConfigs/split-rotate-auto-rename.json
Normal file
33
pipeline/defaultWebUIConfigs/split-rotate-auto-rename.json
Normal 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}"
|
||||||
|
}
|
||||||
@@ -16,7 +16,7 @@ public class PropSync {
|
|||||||
Map<String, String> enProps = linesToProps(enLines);
|
Map<String, String> enProps = linesToProps(enLines);
|
||||||
|
|
||||||
for (File file : files) {
|
for (File file : files) {
|
||||||
if (!file.getName().equals("messages_en_GB.properties")) {
|
if (!"messages_en_GB.properties".equals(file.getName())) {
|
||||||
System.out.println("Processing file: " + file.getName());
|
System.out.println("Processing file: " + file.getName());
|
||||||
List<String> lines;
|
List<String> lines;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import cv2
|
import cv2
|
||||||
import numpy as np
|
|
||||||
import sys
|
import sys
|
||||||
import argparse
|
import argparse
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
def is_blank_image(image_path, threshold=10, white_percent=99, white_value=255, blur_size=5):
|
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)
|
image = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
|
||||||
|
|
||||||
if image is None:
|
if image is None:
|
||||||
print(f"Error: Unable to read the image file: {image_path}")
|
print(f"Error: Unable to read the image file: {image_path}")
|
||||||
return False
|
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)
|
_, thresholded_image = cv2.threshold(blurred_image, white_value - threshold, white_value, cv2.THRESH_BINARY)
|
||||||
|
|
||||||
# Calculate the percentage of white pixels in the thresholded image
|
# Calculate the percentage of white pixels in the thresholded image
|
||||||
white_pixels = 0
|
white_pixels = np.sum(thresholded_image == white_value)
|
||||||
total_pixels = thresholded_image.size
|
white_pixel_percentage = (white_pixels / thresholded_image.size) * 100
|
||||||
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
|
|
||||||
|
|
||||||
print(f"Page has white pixel percent of {white_pixel_percentage}")
|
print(f"Page has white pixel percent of {white_pixel_percentage}")
|
||||||
return True
|
return white_pixel_percentage >= white_percent
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
@@ -40,9 +32,6 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
blank = is_blank_image(args.image_path, args.threshold, args.white_percent)
|
blank = is_blank_image(args.image_path, args.threshold, args.white_percent)
|
||||||
|
|
||||||
if blank:
|
# Return code 1: The image is considered blank.
|
||||||
# Return code 1: The image is considered blank.
|
# Return code 0: The image is not considered blank.
|
||||||
sys.exit(1)
|
sys.exit(int(blank))
|
||||||
else:
|
|
||||||
# Return code 0: The image is not considered blank.
|
|
||||||
sys.exit(0)
|
|
||||||
|
|||||||
19
scripts/download-security-jar.sh
Normal file
19
scripts/download-security-jar.sh
Normal 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/Stirling-Tools/Stirling-PDF/releases/download/v$VERSION_TAG/Stirling-PDF-with-login.jar"
|
||||||
|
curl -L -o app-security.jar https://github.com/Stirling-Tools/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/Stirling-Tools/Stirling-PDF/releases/download/$VERSION_TAG/Stirling-PDF-with-login.jar"
|
||||||
|
curl -L -o app-security.jar https://github.com/Stirling-Tools/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
|
||||||
6
scripts/init-without-ocr.sh
Normal file
6
scripts/init-without-ocr.sh
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
/scripts/download-security-jar.sh
|
||||||
|
|
||||||
|
# Run the main command
|
||||||
|
exec "$@"
|
||||||
@@ -2,8 +2,16 @@
|
|||||||
|
|
||||||
# Copy the original tesseract-ocr files to the volume directory without overwriting existing files
|
# Copy the original tesseract-ocr files to the volume directory without overwriting existing files
|
||||||
echo "Copying original files without overwriting existing files"
|
echo "Copying original files without overwriting existing files"
|
||||||
mkdir -p /usr/share/tesseract-ocr
|
mkdir -p /usr/share/tessdata
|
||||||
cp -rn /usr/share/tesseract-ocr-original/* /usr/share/tesseract-ocr
|
cp -rn /usr/share/tessdata-original/* /usr/share/tessdata
|
||||||
|
|
||||||
|
if [ -d /usr/share/tesseract-ocr/4.00/tessdata ]; then
|
||||||
|
cp -r /usr/share/tesseract-ocr/4.00/tessdata/* /usr/share/tessdata || true;
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -d /usr/share/tesseract-ocr/5/tessdata ]; then
|
||||||
|
cp -r /usr/share/tesseract-ocr/5/tessdata/* /usr/share/tessdata || true;
|
||||||
|
fi
|
||||||
|
|
||||||
# Check if TESSERACT_LANGS environment variable is set and is not empty
|
# Check if TESSERACT_LANGS environment variable is set and is not empty
|
||||||
if [[ -n "$TESSERACT_LANGS" ]]; then
|
if [[ -n "$TESSERACT_LANGS" ]]; then
|
||||||
@@ -16,25 +24,7 @@ if [[ -n "$TESSERACT_LANGS" ]]; then
|
|||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check for DOCKER_ENABLE_SECURITY and download the appropriate JAR if required
|
/scripts/download-security-jar.sh
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
# Run the main command
|
# Run the main command
|
||||||
exec "$@"
|
exec "$@"
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
|
* contributor license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright ownership.
|
||||||
|
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||||
|
* (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.pdfbox.examples.signature;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
|
||||||
|
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
|
||||||
|
import org.bouncycastle.asn1.cms.CMSObjectIdentifiers;
|
||||||
|
import org.bouncycastle.cms.CMSException;
|
||||||
|
import org.bouncycastle.cms.CMSTypedData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps a InputStream into a CMSProcessable object for bouncy castle. It's a memory saving
|
||||||
|
* alternative to the {@link org.bouncycastle.cms.CMSProcessableByteArray CMSProcessableByteArray}
|
||||||
|
* class.
|
||||||
|
*
|
||||||
|
* @author Thomas Chojecki
|
||||||
|
*/
|
||||||
|
class CMSProcessableInputStream implements CMSTypedData {
|
||||||
|
private final InputStream in;
|
||||||
|
private final ASN1ObjectIdentifier contentType;
|
||||||
|
|
||||||
|
CMSProcessableInputStream(InputStream is) {
|
||||||
|
this(new ASN1ObjectIdentifier(CMSObjectIdentifiers.data.getId()), is);
|
||||||
|
}
|
||||||
|
|
||||||
|
CMSProcessableInputStream(ASN1ObjectIdentifier type, InputStream is) {
|
||||||
|
contentType = type;
|
||||||
|
in = is;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object getContent() {
|
||||||
|
return in;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void write(OutputStream out) throws IOException, CMSException {
|
||||||
|
// read the content only one time
|
||||||
|
in.transferTo(out);
|
||||||
|
in.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ASN1ObjectIdentifier getContentType() {
|
||||||
|
return contentType;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2015 The Apache Software Foundation.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.apache.pdfbox.examples.signature;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
import java.security.GeneralSecurityException;
|
||||||
|
import java.security.KeyStore;
|
||||||
|
import java.security.KeyStoreException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.PrivateKey;
|
||||||
|
import java.security.UnrecoverableKeyException;
|
||||||
|
import java.security.cert.Certificate;
|
||||||
|
import java.security.cert.CertificateException;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Enumeration;
|
||||||
|
|
||||||
|
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.SignatureInterface;
|
||||||
|
import org.bouncycastle.cert.jcajce.JcaCertStore;
|
||||||
|
import org.bouncycastle.cms.CMSException;
|
||||||
|
import org.bouncycastle.cms.CMSSignedData;
|
||||||
|
import org.bouncycastle.cms.CMSSignedDataGenerator;
|
||||||
|
import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder;
|
||||||
|
import org.bouncycastle.operator.ContentSigner;
|
||||||
|
import org.bouncycastle.operator.OperatorCreationException;
|
||||||
|
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
|
||||||
|
import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
|
||||||
|
|
||||||
|
public abstract class CreateSignatureBase implements SignatureInterface {
|
||||||
|
private PrivateKey privateKey;
|
||||||
|
private Certificate[] certificateChain;
|
||||||
|
private String tsaUrl;
|
||||||
|
private boolean externalSigning;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the signature creator with a keystore (pkcs12) and pin that should be used for the
|
||||||
|
* signature.
|
||||||
|
*
|
||||||
|
* @param keystore is a pkcs12 keystore.
|
||||||
|
* @param pin is the pin for the keystore / private key
|
||||||
|
* @throws KeyStoreException if the keystore has not been initialized (loaded)
|
||||||
|
* @throws NoSuchAlgorithmException if the algorithm for recovering the key cannot be found
|
||||||
|
* @throws UnrecoverableKeyException if the given password is wrong
|
||||||
|
* @throws CertificateException if the certificate is not valid as signing time
|
||||||
|
* @throws IOException if no certificate could be found
|
||||||
|
*/
|
||||||
|
public CreateSignatureBase(KeyStore keystore, char[] pin)
|
||||||
|
throws KeyStoreException,
|
||||||
|
UnrecoverableKeyException,
|
||||||
|
NoSuchAlgorithmException,
|
||||||
|
IOException,
|
||||||
|
CertificateException {
|
||||||
|
// grabs the first alias from the keystore and get the private key. An
|
||||||
|
// alternative method or constructor could be used for setting a specific
|
||||||
|
// alias that should be used.
|
||||||
|
Enumeration<String> aliases = keystore.aliases();
|
||||||
|
String alias;
|
||||||
|
Certificate cert = null;
|
||||||
|
while (cert == null && aliases.hasMoreElements()) {
|
||||||
|
alias = aliases.nextElement();
|
||||||
|
setPrivateKey((PrivateKey) keystore.getKey(alias, pin));
|
||||||
|
Certificate[] certChain = keystore.getCertificateChain(alias);
|
||||||
|
if (certChain != null) {
|
||||||
|
setCertificateChain(certChain);
|
||||||
|
cert = certChain[0];
|
||||||
|
if (cert instanceof X509Certificate) {
|
||||||
|
// avoid expired certificate
|
||||||
|
((X509Certificate) cert).checkValidity();
|
||||||
|
|
||||||
|
//// SigUtils.checkCertificateUsage((X509Certificate) cert);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cert == null) {
|
||||||
|
throw new IOException("Could not find certificate");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public final void setPrivateKey(PrivateKey privateKey) {
|
||||||
|
this.privateKey = privateKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final void setCertificateChain(final Certificate[] certificateChain) {
|
||||||
|
this.certificateChain = certificateChain;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Certificate[] getCertificateChain() {
|
||||||
|
return certificateChain;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTsaUrl(String tsaUrl) {
|
||||||
|
this.tsaUrl = tsaUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SignatureInterface sample implementation.
|
||||||
|
*
|
||||||
|
* <p>This method will be called from inside of the pdfbox and create the PKCS #7 signature. The
|
||||||
|
* given InputStream contains the bytes that are given by the byte range.
|
||||||
|
*
|
||||||
|
* <p>This method is for internal use only.
|
||||||
|
*
|
||||||
|
* <p>Use your favorite cryptographic library to implement PKCS #7 signature creation. If you
|
||||||
|
* want to create the hash and the signature separately (e.g. to transfer only the hash to an
|
||||||
|
* external application), read <a href="https://stackoverflow.com/questions/41767351">this
|
||||||
|
* answer</a> or <a href="https://stackoverflow.com/questions/56867465">this answer</a>.
|
||||||
|
*
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public byte[] sign(InputStream content) throws IOException {
|
||||||
|
// cannot be done private (interface)
|
||||||
|
try {
|
||||||
|
CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
|
||||||
|
X509Certificate cert = (X509Certificate) certificateChain[0];
|
||||||
|
ContentSigner sha1Signer =
|
||||||
|
new JcaContentSignerBuilder("SHA256WithRSA").build(privateKey);
|
||||||
|
gen.addSignerInfoGenerator(
|
||||||
|
new JcaSignerInfoGeneratorBuilder(
|
||||||
|
new JcaDigestCalculatorProviderBuilder().build())
|
||||||
|
.build(sha1Signer, cert));
|
||||||
|
gen.addCertificates(new JcaCertStore(Arrays.asList(certificateChain)));
|
||||||
|
CMSProcessableInputStream msg = new CMSProcessableInputStream(content);
|
||||||
|
CMSSignedData signedData = gen.generate(msg, false);
|
||||||
|
if (tsaUrl != null && !tsaUrl.isEmpty()) {
|
||||||
|
ValidationTimeStamp validation = new ValidationTimeStamp(tsaUrl);
|
||||||
|
signedData = validation.addSignedTimeStamp(signedData);
|
||||||
|
}
|
||||||
|
return signedData.getEncoded();
|
||||||
|
} catch (GeneralSecurityException
|
||||||
|
| CMSException
|
||||||
|
| OperatorCreationException
|
||||||
|
| URISyntaxException e) {
|
||||||
|
throw new IOException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set if external signing scenario should be used. If {@code false}, SignatureInterface would
|
||||||
|
* be used for signing.
|
||||||
|
*
|
||||||
|
* <p>Default: {@code false}
|
||||||
|
*
|
||||||
|
* @param externalSigning {@code true} if external signing should be performed
|
||||||
|
*/
|
||||||
|
public void setExternalSigning(boolean externalSigning) {
|
||||||
|
this.externalSigning = externalSigning;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isExternalSigning() {
|
||||||
|
return externalSigning;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
|
* contributor license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright ownership.
|
||||||
|
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||||
|
* (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.pdfbox.examples.signature;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.net.URLConnection;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.DigestInputStream;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
|
||||||
|
import org.bouncycastle.operator.DefaultDigestAlgorithmIdentifierFinder;
|
||||||
|
import org.bouncycastle.operator.DigestAlgorithmIdentifierFinder;
|
||||||
|
import org.bouncycastle.tsp.TSPException;
|
||||||
|
import org.bouncycastle.tsp.TimeStampRequest;
|
||||||
|
import org.bouncycastle.tsp.TimeStampRequestGenerator;
|
||||||
|
import org.bouncycastle.tsp.TimeStampResponse;
|
||||||
|
import org.bouncycastle.tsp.TimeStampToken;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Time Stamping Authority (TSA) Client [RFC 3161].
|
||||||
|
*
|
||||||
|
* @author Vakhtang Koroghlishvili
|
||||||
|
* @author John Hewson
|
||||||
|
*/
|
||||||
|
public class TSAClient {
|
||||||
|
private static final Logger LOG = LogManager.getLogger(TSAClient.class);
|
||||||
|
|
||||||
|
private static final DigestAlgorithmIdentifierFinder ALGORITHM_OID_FINDER =
|
||||||
|
new DefaultDigestAlgorithmIdentifierFinder();
|
||||||
|
|
||||||
|
private final URL url;
|
||||||
|
private final String username;
|
||||||
|
private final String password;
|
||||||
|
private final MessageDigest digest;
|
||||||
|
|
||||||
|
// SecureRandom.getInstanceStrong() would be better, but sometimes blocks on Linux
|
||||||
|
private static final Random RANDOM = new SecureRandom();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param url the URL of the TSA service
|
||||||
|
* @param username user name of TSA
|
||||||
|
* @param password password of TSA
|
||||||
|
* @param digest the message digest to use
|
||||||
|
*/
|
||||||
|
public TSAClient(URL url, String username, String password, MessageDigest digest) {
|
||||||
|
this.url = url;
|
||||||
|
this.username = username;
|
||||||
|
this.password = password;
|
||||||
|
this.digest = digest;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param content
|
||||||
|
* @return the time stamp token
|
||||||
|
* @throws IOException if there was an error with the connection or data from the TSA server, or
|
||||||
|
* if the time stamp response could not be validated
|
||||||
|
*/
|
||||||
|
public TimeStampToken getTimeStampToken(InputStream content) throws IOException {
|
||||||
|
digest.reset();
|
||||||
|
DigestInputStream dis = new DigestInputStream(content, digest);
|
||||||
|
while (dis.read() != -1) {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
byte[] hash = digest.digest();
|
||||||
|
|
||||||
|
// 32-bit cryptographic nonce
|
||||||
|
int nonce = RANDOM.nextInt();
|
||||||
|
|
||||||
|
// generate TSA request
|
||||||
|
TimeStampRequestGenerator tsaGenerator = new TimeStampRequestGenerator();
|
||||||
|
tsaGenerator.setCertReq(true);
|
||||||
|
ASN1ObjectIdentifier oid = ALGORITHM_OID_FINDER.find(digest.getAlgorithm()).getAlgorithm();
|
||||||
|
TimeStampRequest request = tsaGenerator.generate(oid, hash, BigInteger.valueOf(nonce));
|
||||||
|
|
||||||
|
// get TSA response
|
||||||
|
byte[] tsaResponse = getTSAResponse(request.getEncoded());
|
||||||
|
|
||||||
|
TimeStampResponse response;
|
||||||
|
try {
|
||||||
|
response = new TimeStampResponse(tsaResponse);
|
||||||
|
response.validate(request);
|
||||||
|
} catch (TSPException e) {
|
||||||
|
throw new IOException(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
TimeStampToken timeStampToken = response.getTimeStampToken();
|
||||||
|
if (timeStampToken == null) {
|
||||||
|
// https://www.ietf.org/rfc/rfc3161.html#section-2.4.2
|
||||||
|
throw new IOException(
|
||||||
|
"Response from "
|
||||||
|
+ url
|
||||||
|
+ " does not have a time stamp token, status: "
|
||||||
|
+ response.getStatus()
|
||||||
|
+ " ("
|
||||||
|
+ response.getStatusString()
|
||||||
|
+ ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
return timeStampToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
// gets response data for the given encoded TimeStampRequest data
|
||||||
|
// throws IOException if a connection to the TSA cannot be established
|
||||||
|
private byte[] getTSAResponse(byte[] request) throws IOException {
|
||||||
|
LOG.debug("Opening connection to TSA server");
|
||||||
|
|
||||||
|
// todo: support proxy servers
|
||||||
|
URLConnection connection = url.openConnection();
|
||||||
|
connection.setDoOutput(true);
|
||||||
|
connection.setDoInput(true);
|
||||||
|
connection.setRequestProperty("Content-Type", "application/timestamp-query");
|
||||||
|
|
||||||
|
LOG.debug("Established connection to TSA server");
|
||||||
|
|
||||||
|
if (username != null && password != null && !username.isEmpty() && !password.isEmpty()) {
|
||||||
|
String contentEncoding = connection.getContentEncoding();
|
||||||
|
if (contentEncoding == null) {
|
||||||
|
contentEncoding = StandardCharsets.UTF_8.name();
|
||||||
|
}
|
||||||
|
connection.setRequestProperty(
|
||||||
|
"Authorization",
|
||||||
|
"Basic "
|
||||||
|
+ new String(
|
||||||
|
Base64.getEncoder()
|
||||||
|
.encode(
|
||||||
|
(username + ":" + password)
|
||||||
|
.getBytes(contentEncoding))));
|
||||||
|
}
|
||||||
|
|
||||||
|
// read response
|
||||||
|
try (OutputStream output = connection.getOutputStream()) {
|
||||||
|
output.write(request);
|
||||||
|
} catch (IOException ex) {
|
||||||
|
LOG.error("Exception when writing to {}", this.url, ex);
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG.debug("Waiting for response from TSA server");
|
||||||
|
|
||||||
|
byte[] response;
|
||||||
|
try (InputStream input = connection.getInputStream()) {
|
||||||
|
response = input.readAllBytes();
|
||||||
|
} catch (IOException ex) {
|
||||||
|
LOG.error("Exception when reading from {}", this.url, ex);
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG.debug("Received response from TSA server");
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
|
* contributor license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright ownership.
|
||||||
|
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||||
|
* (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.apache.pdfbox.examples.signature;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.net.MalformedURLException;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.bouncycastle.asn1.ASN1Encodable;
|
||||||
|
import org.bouncycastle.asn1.ASN1EncodableVector;
|
||||||
|
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
|
||||||
|
import org.bouncycastle.asn1.ASN1Primitive;
|
||||||
|
import org.bouncycastle.asn1.DERSet;
|
||||||
|
import org.bouncycastle.asn1.cms.Attribute;
|
||||||
|
import org.bouncycastle.asn1.cms.AttributeTable;
|
||||||
|
import org.bouncycastle.asn1.cms.Attributes;
|
||||||
|
import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
|
||||||
|
import org.bouncycastle.cms.CMSSignedData;
|
||||||
|
import org.bouncycastle.cms.SignerInformation;
|
||||||
|
import org.bouncycastle.cms.SignerInformationStore;
|
||||||
|
import org.bouncycastle.tsp.TimeStampToken;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class wraps the TSAClient and the work that has to be done with it. Like Adding Signed
|
||||||
|
* TimeStamps to a signature, or creating a CMS timestamp attribute (with a signed timestamp)
|
||||||
|
*
|
||||||
|
* @author Others
|
||||||
|
* @author Alexis Suter
|
||||||
|
*/
|
||||||
|
public class ValidationTimeStamp {
|
||||||
|
private TSAClient tsaClient;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param tsaUrl The url where TS-Request will be done.
|
||||||
|
* @throws NoSuchAlgorithmException
|
||||||
|
* @throws MalformedURLException
|
||||||
|
* @throws java.net.URISyntaxException
|
||||||
|
*/
|
||||||
|
public ValidationTimeStamp(String tsaUrl)
|
||||||
|
throws NoSuchAlgorithmException, MalformedURLException, URISyntaxException {
|
||||||
|
if (tsaUrl != null) {
|
||||||
|
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||||
|
this.tsaClient = new TSAClient(new URI(tsaUrl).toURL(), null, null, digest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a signed timestamp token by the given input stream.
|
||||||
|
*
|
||||||
|
* @param content InputStream of the content to sign
|
||||||
|
* @return the byte[] of the timestamp token
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public byte[] getTimeStampToken(InputStream content) throws IOException {
|
||||||
|
TimeStampToken timeStampToken = tsaClient.getTimeStampToken(content);
|
||||||
|
return timeStampToken.getEncoded();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extend cms signed data with TimeStamp first or to all signers
|
||||||
|
*
|
||||||
|
* @param signedData Generated CMS signed data
|
||||||
|
* @return CMSSignedData Extended CMS signed data
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public CMSSignedData addSignedTimeStamp(CMSSignedData signedData) throws IOException {
|
||||||
|
SignerInformationStore signerStore = signedData.getSignerInfos();
|
||||||
|
List<SignerInformation> newSigners = new ArrayList<>();
|
||||||
|
|
||||||
|
for (SignerInformation signer : signerStore.getSigners()) {
|
||||||
|
// This adds a timestamp to every signer (into his unsigned attributes) in the
|
||||||
|
// signature.
|
||||||
|
newSigners.add(signTimeStamp(signer));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Because new SignerInformation is created, new SignerInfoStore has to be created
|
||||||
|
// and also be replaced in signedData. Which creates a new signedData object.
|
||||||
|
return CMSSignedData.replaceSigners(signedData, new SignerInformationStore(newSigners));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extend CMS Signer Information with the TimeStampToken into the unsigned Attributes.
|
||||||
|
*
|
||||||
|
* @param signer information about signer
|
||||||
|
* @return information about SignerInformation
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
private SignerInformation signTimeStamp(SignerInformation signer) throws IOException {
|
||||||
|
AttributeTable unsignedAttributes = signer.getUnsignedAttributes();
|
||||||
|
|
||||||
|
ASN1EncodableVector vector = new ASN1EncodableVector();
|
||||||
|
if (unsignedAttributes != null) {
|
||||||
|
vector = unsignedAttributes.toASN1EncodableVector();
|
||||||
|
}
|
||||||
|
|
||||||
|
TimeStampToken timeStampToken =
|
||||||
|
tsaClient.getTimeStampToken(new ByteArrayInputStream(signer.getSignature()));
|
||||||
|
byte[] token = timeStampToken.getEncoded();
|
||||||
|
ASN1ObjectIdentifier oid = PKCSObjectIdentifiers.id_aa_signatureTimeStampToken;
|
||||||
|
ASN1Encodable signatureTimeStamp =
|
||||||
|
new Attribute(oid, new DERSet(ASN1Primitive.fromByteArray(token)));
|
||||||
|
|
||||||
|
vector.add(signatureTimeStamp);
|
||||||
|
Attributes signedAttributes = new Attributes(vector);
|
||||||
|
|
||||||
|
// There is no other way changing the unsigned attributes of the signer information.
|
||||||
|
// result is never null, new SignerInformation always returned,
|
||||||
|
// see source code of replaceUnsignedAttributes
|
||||||
|
return SignerInformation.replaceUnsignedAttributes(
|
||||||
|
signer, new AttributeTable(signedAttributes));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
|
* contributor license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright ownership.
|
||||||
|
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||||
|
* (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.pdfbox.examples.util;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delegate class to close the connection when the class gets closed.
|
||||||
|
*
|
||||||
|
* @author Tilman Hausherr
|
||||||
|
*/
|
||||||
|
public class ConnectedInputStream extends InputStream {
|
||||||
|
HttpURLConnection con;
|
||||||
|
InputStream is;
|
||||||
|
|
||||||
|
public ConnectedInputStream(HttpURLConnection con, InputStream is) {
|
||||||
|
this.con = con;
|
||||||
|
this.is = is;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int read() throws IOException {
|
||||||
|
return is.read();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int read(byte[] b) throws IOException {
|
||||||
|
return is.read(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int read(byte[] b, int off, int len) throws IOException {
|
||||||
|
return is.read(b, off, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long skip(long n) throws IOException {
|
||||||
|
return is.skip(n);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int available() throws IOException {
|
||||||
|
return is.available();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void mark(int readlimit) {
|
||||||
|
is.mark(readlimit);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void reset() throws IOException {
|
||||||
|
is.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean markSupported() {
|
||||||
|
return is.markSupported();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws IOException {
|
||||||
|
is.close();
|
||||||
|
con.disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,8 @@ import java.net.Socket;
|
|||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
|
import io.github.pixee.security.SystemCommand;
|
||||||
|
|
||||||
public class LibreOfficeListener {
|
public class LibreOfficeListener {
|
||||||
|
|
||||||
private static final long ACTIVITY_TIMEOUT = 20 * 60 * 1000; // 20 minutes
|
private static final long ACTIVITY_TIMEOUT = 20 * 60 * 1000; // 20 minutes
|
||||||
@@ -22,14 +24,14 @@ public class LibreOfficeListener {
|
|||||||
|
|
||||||
private Process process;
|
private Process process;
|
||||||
|
|
||||||
private LibreOfficeListener() {
|
private LibreOfficeListener() {}
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isListenerRunning() {
|
private boolean isListenerRunning() {
|
||||||
try {
|
try {
|
||||||
System.out.println("waiting for listener to start");
|
System.out.println("waiting for listener to start");
|
||||||
Socket socket = new Socket();
|
Socket socket = new Socket();
|
||||||
socket.connect(new InetSocketAddress("localhost", 2002), 1000); // Timeout after 1 second
|
socket.connect(
|
||||||
|
new InetSocketAddress("localhost", 2002), 1000); // Timeout after 1 second
|
||||||
socket.close();
|
socket.close();
|
||||||
return true;
|
return true;
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
@@ -44,26 +46,27 @@ public class LibreOfficeListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Start the listener process
|
// Start the listener process
|
||||||
process = Runtime.getRuntime().exec("unoconv --listener");
|
process = SystemCommand.runCommand(Runtime.getRuntime(), "unoconv --listener");
|
||||||
lastActivityTime = System.currentTimeMillis();
|
lastActivityTime = System.currentTimeMillis();
|
||||||
|
|
||||||
// Start a background thread to monitor the activity timeout
|
// Start a background thread to monitor the activity timeout
|
||||||
executorService = Executors.newSingleThreadExecutor();
|
executorService = Executors.newSingleThreadExecutor();
|
||||||
executorService.submit(() -> {
|
executorService.submit(
|
||||||
while (true) {
|
() -> {
|
||||||
long idleTime = System.currentTimeMillis() - lastActivityTime;
|
while (true) {
|
||||||
if (idleTime >= ACTIVITY_TIMEOUT) {
|
long idleTime = System.currentTimeMillis() - lastActivityTime;
|
||||||
// If there has been no activity for too long, tear down the listener
|
if (idleTime >= ACTIVITY_TIMEOUT) {
|
||||||
process.destroy();
|
// If there has been no activity for too long, tear down the listener
|
||||||
break;
|
process.destroy();
|
||||||
}
|
break;
|
||||||
try {
|
}
|
||||||
Thread.sleep(5000); // Check for inactivity every 5 seconds
|
try {
|
||||||
} catch (InterruptedException e) {
|
Thread.sleep(5000); // Check for inactivity every 5 seconds
|
||||||
break;
|
} catch (InterruptedException e) {
|
||||||
}
|
break;
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Wait for the listener to start up
|
// Wait for the listener to start up
|
||||||
long startTime = System.currentTimeMillis();
|
long startTime = System.currentTimeMillis();
|
||||||
@@ -92,5 +95,4 @@ public class LibreOfficeListener {
|
|||||||
process.destroy();
|
process.destroy();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,82 +1,83 @@
|
|||||||
package stirling.software.SPDF;
|
package stirling.software.SPDF;
|
||||||
|
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
import org.springframework.core.env.Environment;
|
import org.springframework.core.env.Environment;
|
||||||
|
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||||
import jakarta.annotation.PostConstruct;
|
|
||||||
import stirling.software.SPDF.config.ConfigInitializer;
|
import io.github.pixee.security.SystemCommand;
|
||||||
import stirling.software.SPDF.utils.GeneralUtils;
|
|
||||||
@SpringBootApplication
|
import jakarta.annotation.PostConstruct;
|
||||||
|
import stirling.software.SPDF.config.ConfigInitializer;
|
||||||
//@EnableScheduling
|
import stirling.software.SPDF.utils.GeneralUtils;
|
||||||
public class SPdfApplication {
|
|
||||||
|
@SpringBootApplication
|
||||||
@Autowired
|
@EnableScheduling
|
||||||
private Environment env;
|
public class SPdfApplication {
|
||||||
|
|
||||||
@PostConstruct
|
@Autowired private Environment env;
|
||||||
public void init() {
|
|
||||||
// Check if the BROWSER_OPEN environment variable is set to true
|
@PostConstruct
|
||||||
String browserOpenEnv = env.getProperty("BROWSER_OPEN");
|
public void init() {
|
||||||
boolean browserOpen = browserOpenEnv != null && browserOpenEnv.equalsIgnoreCase("true");
|
// Check if the BROWSER_OPEN environment variable is set to true
|
||||||
|
String browserOpenEnv = env.getProperty("BROWSER_OPEN");
|
||||||
if (browserOpen) {
|
boolean browserOpen = browserOpenEnv != null && "true".equalsIgnoreCase(browserOpenEnv);
|
||||||
try {
|
|
||||||
String port = env.getProperty("local.server.port");
|
if (browserOpen) {
|
||||||
if(port == null || port.length() == 0) {
|
try {
|
||||||
port="8080";
|
String url = "http://localhost:" + getPort();
|
||||||
}
|
|
||||||
String url = "http://localhost:" + port;
|
String os = System.getProperty("os.name").toLowerCase();
|
||||||
|
Runtime rt = Runtime.getRuntime();
|
||||||
String os = System.getProperty("os.name").toLowerCase();
|
if (os.contains("win")) {
|
||||||
Runtime rt = Runtime.getRuntime();
|
// For Windows
|
||||||
if (os.contains("win")) {
|
SystemCommand.runCommand(rt, "rundll32 url.dll,FileProtocolHandler " + url);
|
||||||
// For Windows
|
}
|
||||||
rt.exec("rundll32 url.dll,FileProtocolHandler " + url);
|
} catch (Exception e) {
|
||||||
}
|
e.printStackTrace();
|
||||||
} catch (Exception e) {
|
}
|
||||||
e.printStackTrace();
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
public static void main(String[] args) {
|
||||||
|
SpringApplication app = new SpringApplication(SPdfApplication.class);
|
||||||
public static void main(String[] args) {
|
app.addInitializers(new ConfigInitializer());
|
||||||
SpringApplication app = new SpringApplication(SPdfApplication.class);
|
if (Files.exists(Paths.get("configs/settings.yml"))) {
|
||||||
app.addInitializers(new ConfigInitializer());
|
app.setDefaultProperties(
|
||||||
if (Files.exists(Paths.get("configs/settings.yml"))) {
|
Collections.singletonMap(
|
||||||
app.setDefaultProperties(Collections.singletonMap("spring.config.additional-location", "file:configs/settings.yml"));
|
"spring.config.additional-location", "file:configs/settings.yml"));
|
||||||
} else {
|
} else {
|
||||||
System.out.println("External configuration file 'configs/settings.yml' does not exist. Using default configuration and environment configuration instead.");
|
System.out.println(
|
||||||
}
|
"External configuration file 'configs/settings.yml' does not exist. Using default configuration and environment configuration instead.");
|
||||||
app.run(args);
|
}
|
||||||
|
app.run(args);
|
||||||
try {
|
|
||||||
Thread.sleep(1000);
|
try {
|
||||||
} catch (InterruptedException e) {
|
Thread.sleep(1000);
|
||||||
// TODO Auto-generated catch block
|
} catch (InterruptedException e) {
|
||||||
e.printStackTrace();
|
// TODO Auto-generated catch block
|
||||||
}
|
e.printStackTrace();
|
||||||
|
}
|
||||||
GeneralUtils.createDir("customFiles/static/");
|
|
||||||
GeneralUtils.createDir("customFiles/templates/");
|
GeneralUtils.createDir("customFiles/static/");
|
||||||
|
GeneralUtils.createDir("customFiles/templates/");
|
||||||
|
|
||||||
|
System.out.println("Stirling-PDF Started.");
|
||||||
System.out.println("Stirling-PDF Started.");
|
|
||||||
|
String url = "http://localhost:" + getPort();
|
||||||
String port = System.getProperty("local.server.port");
|
System.out.println("Navigate to " + url);
|
||||||
if(port == null || port.length() == 0) {
|
}
|
||||||
port="8080";
|
|
||||||
}
|
public static String getPort() {
|
||||||
String url = "http://localhost:" + port;
|
String port = System.getProperty("local.server.port");
|
||||||
System.out.println("Navigate to " + url);
|
if (port == null || port.isEmpty()) {
|
||||||
}
|
port = "8080";
|
||||||
|
}
|
||||||
|
return port;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,53 +1,87 @@
|
|||||||
package stirling.software.SPDF.config;
|
package stirling.software.SPDF.config;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import java.io.IOException;
|
||||||
import org.springframework.context.annotation.Bean;
|
import java.nio.file.Files;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import java.nio.file.Paths;
|
||||||
|
import java.util.Properties;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
|
||||||
@Configuration
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
public class AppConfig {
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
@Autowired
|
import org.springframework.core.io.ClassPathResource;
|
||||||
ApplicationProperties applicationProperties;
|
import org.springframework.core.io.Resource;
|
||||||
|
|
||||||
@Bean(name = "loginEnabled")
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
public boolean loginEnabled() {
|
|
||||||
return applicationProperties.getSecurity().getEnableLogin();
|
@Configuration
|
||||||
}
|
public class AppConfig {
|
||||||
|
|
||||||
@Bean(name = "appName")
|
@Autowired ApplicationProperties applicationProperties;
|
||||||
public String appName() {
|
|
||||||
String homeTitle = applicationProperties.getUi().getAppName();
|
@Bean(name = "loginEnabled")
|
||||||
return (homeTitle != null) ? homeTitle : "Stirling PDF";
|
public boolean loginEnabled() {
|
||||||
}
|
return applicationProperties.getSecurity().getEnableLogin();
|
||||||
|
}
|
||||||
@Bean(name = "appVersion")
|
|
||||||
public String appVersion() {
|
@Bean(name = "appName")
|
||||||
String version = getClass().getPackage().getImplementationVersion();
|
public String appName() {
|
||||||
return (version != null) ? version : "0.0.0";
|
String homeTitle = applicationProperties.getUi().getAppName();
|
||||||
}
|
return (homeTitle != null) ? homeTitle : "Stirling PDF";
|
||||||
|
}
|
||||||
@Bean(name = "homeText")
|
|
||||||
public String homeText() {
|
@Bean(name = "appVersion")
|
||||||
return (applicationProperties.getUi().getHomeDescription() != null) ? applicationProperties.getUi().getHomeDescription() : "null";
|
public String appVersion() {
|
||||||
}
|
Resource resource = new ClassPathResource("version.properties");
|
||||||
|
Properties props = new Properties();
|
||||||
|
try {
|
||||||
@Bean(name = "navBarText")
|
props.load(resource.getInputStream());
|
||||||
public String navBarText() {
|
return props.getProperty("version");
|
||||||
String defaultNavBar = applicationProperties.getUi().getAppNameNavbar() != null ? applicationProperties.getUi().getAppNameNavbar() : applicationProperties.getUi().getAppName();
|
} catch (IOException e) {
|
||||||
return (defaultNavBar != null) ? defaultNavBar : "Stirling PDF";
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
|
return "0.0.0";
|
||||||
@Bean(name = "rateLimit")
|
}
|
||||||
public boolean rateLimit() {
|
|
||||||
String appName = System.getProperty("rateLimit");
|
@Bean(name = "homeText")
|
||||||
if (appName == null)
|
public String homeText() {
|
||||||
appName = System.getenv("rateLimit");
|
return (applicationProperties.getUi().getHomeDescription() != null)
|
||||||
System.out.println("rateLimit=" + appName);
|
? applicationProperties.getUi().getHomeDescription()
|
||||||
return (appName != null) ? Boolean.valueOf(appName) : false;
|
: "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;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean(name = "RunningInDocker")
|
||||||
|
public boolean runningInDocker() {
|
||||||
|
return Files.exists(Paths.get("/.dockerenv"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean(name = "bookAndHtmlFormatsInstalled")
|
||||||
|
public boolean bookAndHtmlFormatsInstalled() {
|
||||||
|
return applicationProperties
|
||||||
|
.getSystem()
|
||||||
|
.getCustomApplications()
|
||||||
|
.isInstallBookAndHtmlFormats();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,65 +1,64 @@
|
|||||||
package stirling.software.SPDF.config;
|
package stirling.software.SPDF.config;
|
||||||
|
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.web.servlet.LocaleResolver;
|
import org.springframework.web.servlet.LocaleResolver;
|
||||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
|
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
|
||||||
import org.springframework.web.servlet.i18n.SessionLocaleResolver;
|
import org.springframework.web.servlet.i18n.SessionLocaleResolver;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
public class Beans implements WebMvcConfigurer {
|
public class Beans implements WebMvcConfigurer {
|
||||||
|
|
||||||
@Autowired
|
@Autowired ApplicationProperties applicationProperties;
|
||||||
ApplicationProperties applicationProperties;
|
|
||||||
|
@Override
|
||||||
@Override
|
public void addInterceptors(InterceptorRegistry registry) {
|
||||||
public void addInterceptors(InterceptorRegistry registry) {
|
registry.addInterceptor(localeChangeInterceptor());
|
||||||
registry.addInterceptor(localeChangeInterceptor());
|
registry.addInterceptor(new CleanUrlInterceptor());
|
||||||
registry.addInterceptor(new CleanUrlInterceptor());
|
}
|
||||||
}
|
|
||||||
|
@Bean
|
||||||
@Bean
|
public LocaleChangeInterceptor localeChangeInterceptor() {
|
||||||
public LocaleChangeInterceptor localeChangeInterceptor() {
|
LocaleChangeInterceptor lci = new LocaleChangeInterceptor();
|
||||||
LocaleChangeInterceptor lci = new LocaleChangeInterceptor();
|
lci.setParamName("lang");
|
||||||
lci.setParamName("lang");
|
return lci;
|
||||||
return lci;
|
}
|
||||||
}
|
|
||||||
|
@Bean
|
||||||
@Bean
|
public LocaleResolver localeResolver() {
|
||||||
public LocaleResolver localeResolver() {
|
SessionLocaleResolver slr = new SessionLocaleResolver();
|
||||||
SessionLocaleResolver slr = new SessionLocaleResolver();
|
|
||||||
|
String appLocaleEnv = applicationProperties.getSystem().getDefaultLocale();
|
||||||
|
Locale defaultLocale =
|
||||||
String appLocaleEnv = applicationProperties.getSystem().getDefaultLocale();
|
Locale.UK; // Fallback to UK locale if environment variable is not set
|
||||||
Locale defaultLocale = Locale.UK; // Fallback to UK locale if environment variable is not set
|
|
||||||
|
if (appLocaleEnv != null && !appLocaleEnv.isEmpty()) {
|
||||||
if (appLocaleEnv != null && !appLocaleEnv.isEmpty()) {
|
Locale tempLocale = Locale.forLanguageTag(appLocaleEnv);
|
||||||
Locale tempLocale = Locale.forLanguageTag(appLocaleEnv);
|
String tempLanguageTag = tempLocale.toLanguageTag();
|
||||||
String tempLanguageTag = tempLocale.toLanguageTag();
|
|
||||||
|
if (appLocaleEnv.equalsIgnoreCase(tempLanguageTag)) {
|
||||||
if (appLocaleEnv.equalsIgnoreCase(tempLanguageTag)) {
|
defaultLocale = tempLocale;
|
||||||
defaultLocale = tempLocale;
|
} else {
|
||||||
} else {
|
tempLocale = Locale.forLanguageTag(appLocaleEnv.replace("_", "-"));
|
||||||
tempLocale = Locale.forLanguageTag(appLocaleEnv.replace("_","-"));
|
tempLanguageTag = tempLocale.toLanguageTag();
|
||||||
tempLanguageTag = tempLocale.toLanguageTag();
|
|
||||||
|
if (appLocaleEnv.equalsIgnoreCase(tempLanguageTag)) {
|
||||||
if (appLocaleEnv.equalsIgnoreCase(tempLanguageTag)) {
|
defaultLocale = tempLocale;
|
||||||
defaultLocale = tempLocale;
|
} else {
|
||||||
} else {
|
System.err.println(
|
||||||
System.err.println("Invalid APP_LOCALE environment variable value. Falling back to default Locale.UK.");
|
"Invalid APP_LOCALE environment variable value. Falling back to default Locale.UK.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
slr.setDefaultLocale(defaultLocale);
|
slr.setDefaultLocale(defaultLocale);
|
||||||
return slr;
|
return slr;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,68 +1,74 @@
|
|||||||
package stirling.software.SPDF.config;
|
package stirling.software.SPDF.config;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import org.springframework.web.servlet.HandlerInterceptor;
|
import org.springframework.web.servlet.HandlerInterceptor;
|
||||||
import org.springframework.web.servlet.ModelAndView;
|
import org.springframework.web.servlet.ModelAndView;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
public class CleanUrlInterceptor implements HandlerInterceptor {
|
public class CleanUrlInterceptor implements HandlerInterceptor {
|
||||||
|
|
||||||
private static final List<String> ALLOWED_PARAMS = Arrays.asList("lang", "endpoint", "endpoints", "logout", "error", "file", "messageType");
|
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)
|
@Override
|
||||||
throws Exception {
|
public boolean preHandle(
|
||||||
String queryString = request.getQueryString();
|
HttpServletRequest request, HttpServletResponse response, Object handler)
|
||||||
if (queryString != null && !queryString.isEmpty()) {
|
throws Exception {
|
||||||
String requestURI = request.getRequestURI();
|
String queryString = request.getQueryString();
|
||||||
Map<String, String> parameters = new HashMap<>();
|
if (queryString != null && !queryString.isEmpty()) {
|
||||||
|
String requestURI = request.getRequestURI();
|
||||||
// Keep only the allowed parameters
|
Map<String, String> parameters = new HashMap<>();
|
||||||
String[] queryParameters = queryString.split("&");
|
|
||||||
for (String param : queryParameters) {
|
// Keep only the allowed parameters
|
||||||
String[] keyValue = param.split("=");
|
String[] queryParameters = queryString.split("&");
|
||||||
if (keyValue.length != 2) {
|
for (String param : queryParameters) {
|
||||||
continue;
|
String[] keyValue = param.split("=");
|
||||||
}
|
if (keyValue.length != 2) {
|
||||||
if (ALLOWED_PARAMS.contains(keyValue[0])) {
|
continue;
|
||||||
parameters.put(keyValue[0], keyValue[1]);
|
}
|
||||||
}
|
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
|
// If there are any parameters that are not allowed
|
||||||
StringBuilder newQueryString = new StringBuilder();
|
if (parameters.size() != queryParameters.length) {
|
||||||
for (Map.Entry<String, String> entry : parameters.entrySet()) {
|
// Construct new query string
|
||||||
if (newQueryString.length() > 0) {
|
StringBuilder newQueryString = new StringBuilder();
|
||||||
newQueryString.append("&");
|
for (Map.Entry<String, String> entry : parameters.entrySet()) {
|
||||||
}
|
if (newQueryString.length() > 0) {
|
||||||
newQueryString.append(entry.getKey()).append("=").append(entry.getValue());
|
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);
|
// Redirect to the URL with only allowed query parameters
|
||||||
return false;
|
String redirectUrl = requestURI + "?" + newQueryString;
|
||||||
}
|
response.sendRedirect(redirectUrl);
|
||||||
}
|
return false;
|
||||||
return true;
|
}
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
@Override
|
}
|
||||||
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
|
|
||||||
ModelAndView modelAndView) {
|
@Override
|
||||||
}
|
public void postHandle(
|
||||||
|
HttpServletRequest request,
|
||||||
@Override
|
HttpServletResponse response,
|
||||||
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
|
Object handler,
|
||||||
Exception ex) {
|
ModelAndView modelAndView) {}
|
||||||
}
|
|
||||||
}
|
@Override
|
||||||
|
public void afterCompletion(
|
||||||
|
HttpServletRequest request,
|
||||||
|
HttpServletResponse response,
|
||||||
|
Object handler,
|
||||||
|
Exception ex) {}
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,12 +12,15 @@ import java.nio.file.Paths;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.function.Function;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.springframework.context.ApplicationContextInitializer;
|
import org.springframework.context.ApplicationContextInitializer;
|
||||||
import org.springframework.context.ConfigurableApplicationContext;
|
import org.springframework.context.ConfigurableApplicationContext;
|
||||||
|
|
||||||
public class ConfigInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
|
public class ConfigInitializer
|
||||||
|
implements ApplicationContextInitializer<ConfigurableApplicationContext> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void initialize(ConfigurableApplicationContext applicationContext) {
|
public void initialize(ConfigurableApplicationContext applicationContext) {
|
||||||
@@ -38,64 +41,103 @@ public class ConfigInitializer implements ApplicationContextInitializer<Configur
|
|||||||
Files.createDirectories(destPath.getParent());
|
Files.createDirectories(destPath.getParent());
|
||||||
|
|
||||||
// Copy the resource from classpath to the external directory
|
// Copy the resource from classpath to the external directory
|
||||||
try (InputStream in = getClass().getClassLoader().getResourceAsStream("settings.yml.template")) {
|
try (InputStream in =
|
||||||
|
getClass().getClassLoader().getResourceAsStream("settings.yml.template")) {
|
||||||
if (in != null) {
|
if (in != null) {
|
||||||
Files.copy(in, destPath);
|
Files.copy(in, destPath);
|
||||||
} else {
|
} else {
|
||||||
throw new FileNotFoundException("Resource file not found: settings.yml.template");
|
throw new FileNotFoundException(
|
||||||
|
"Resource file not found: settings.yml.template");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// If user file exists, we need to merge it with the template from the classpath
|
// If user file exists, we need to merge it with the template from the classpath
|
||||||
List<String> templateLines;
|
List<String> templateLines;
|
||||||
try (InputStream in = getClass().getClassLoader().getResourceAsStream("settings.yml.template")) {
|
try (InputStream in =
|
||||||
templateLines = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8)).lines().collect(Collectors.toList());
|
getClass().getClassLoader().getResourceAsStream("settings.yml.template")) {
|
||||||
|
templateLines =
|
||||||
|
new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))
|
||||||
|
.lines()
|
||||||
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
mergeYamlFiles(templateLines, destPath, destPath);
|
mergeYamlFiles(templateLines, destPath, destPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void mergeYamlFiles(List<String> templateLines, Path userFilePath, Path outputPath) throws IOException {
|
public void mergeYamlFiles(List<String> templateLines, Path userFilePath, Path outputPath)
|
||||||
|
throws IOException {
|
||||||
List<String> userLines = Files.readAllLines(userFilePath);
|
List<String> userLines = Files.readAllLines(userFilePath);
|
||||||
|
|
||||||
List<String> mergedLines = new ArrayList<>();
|
List<String> mergedLines = new ArrayList<>();
|
||||||
boolean insideAutoGenerated = false;
|
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) {
|
for (String line : templateLines) {
|
||||||
// Check if we've entered or left the AutomaticallyGenerated section
|
String key = extractKey.apply(line);
|
||||||
if (line.trim().equalsIgnoreCase("AutomaticallyGenerated:")) {
|
|
||||||
|
if ("AutomaticallyGenerated:".equalsIgnoreCase(line.trim())) {
|
||||||
insideAutoGenerated = true;
|
insideAutoGenerated = true;
|
||||||
mergedLines.add(line);
|
mergedLines.add(line);
|
||||||
continue;
|
continue;
|
||||||
} else if (insideAutoGenerated && line.trim().isEmpty()) {
|
} else if (insideAutoGenerated && line.trim().isEmpty()) {
|
||||||
// We have reached the end of the AutomaticallyGenerated section
|
|
||||||
insideAutoGenerated = false;
|
insideAutoGenerated = false;
|
||||||
mergedLines.add(line);
|
mergedLines.add(line);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (insideAutoGenerated) {
|
if (beforeFirstKey && (isCommented.apply(line) || line.trim().isEmpty())) {
|
||||||
// Add lines from user's settings if we are inside AutomaticallyGenerated
|
// Handle top comments and empty lines before the first key.
|
||||||
Optional<String> userAutoGenValue = userLines.stream().filter(l -> l.trim().startsWith(line.split(":")[0].trim())).findFirst();
|
|
||||||
if (userAutoGenValue.isPresent()) {
|
|
||||||
mergedLines.add(userAutoGenValue.get());
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Outside of AutomaticallyGenerated, continue as before
|
|
||||||
if (line.contains(": ")) {
|
|
||||||
String key = line.split(": ")[0].trim();
|
|
||||||
Optional<String> userValue = userLines.stream().filter(l -> l.trim().startsWith(key)).findFirst();
|
|
||||||
if (userValue.isPresent()) {
|
|
||||||
mergedLines.add(userValue.get());
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mergedLines.add(line);
|
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);
|
Files.write(outputPath, mergedLines, StandardCharsets.UTF_8);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,228 +1,247 @@
|
|||||||
package stirling.software.SPDF.config;
|
package stirling.software.SPDF.config;
|
||||||
|
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
|
import org.springframework.context.annotation.DependsOn;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import org.springframework.stereotype.Service;
|
||||||
@Service
|
|
||||||
public class EndpointConfiguration {
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
private static final Logger logger = LoggerFactory.getLogger(EndpointConfiguration.class);
|
|
||||||
private Map<String, Boolean> endpointStatuses = new ConcurrentHashMap<>();
|
@Service
|
||||||
private Map<String, Set<String>> endpointGroups = new ConcurrentHashMap<>();
|
@DependsOn({"bookAndHtmlFormatsInstalled"})
|
||||||
|
public class EndpointConfiguration {
|
||||||
private final ApplicationProperties applicationProperties;
|
private static final Logger logger = LoggerFactory.getLogger(EndpointConfiguration.class);
|
||||||
|
private Map<String, Boolean> endpointStatuses = new ConcurrentHashMap<>();
|
||||||
@Autowired
|
private Map<String, Set<String>> endpointGroups = new ConcurrentHashMap<>();
|
||||||
public EndpointConfiguration(ApplicationProperties applicationProperties) {
|
|
||||||
this.applicationProperties = applicationProperties;
|
private final ApplicationProperties applicationProperties;
|
||||||
init();
|
|
||||||
processEnvironmentConfigs();
|
private boolean bookAndHtmlFormatsInstalled;
|
||||||
}
|
|
||||||
|
@Autowired
|
||||||
public void enableEndpoint(String endpoint) {
|
public EndpointConfiguration(
|
||||||
endpointStatuses.put(endpoint, true);
|
ApplicationProperties applicationProperties,
|
||||||
}
|
@Qualifier("bookAndHtmlFormatsInstalled") boolean bookAndHtmlFormatsInstalled) {
|
||||||
|
this.applicationProperties = applicationProperties;
|
||||||
public void disableEndpoint(String endpoint) {
|
this.bookAndHtmlFormatsInstalled = bookAndHtmlFormatsInstalled;
|
||||||
if(!endpointStatuses.containsKey(endpoint) || endpointStatuses.get(endpoint) != false) {
|
init();
|
||||||
logger.info("Disabling {}", endpoint);
|
processEnvironmentConfigs();
|
||||||
endpointStatuses.put(endpoint, false);
|
}
|
||||||
}
|
|
||||||
}
|
public void enableEndpoint(String endpoint) {
|
||||||
|
endpointStatuses.put(endpoint, true);
|
||||||
public boolean isEndpointEnabled(String endpoint) {
|
}
|
||||||
if (endpoint.startsWith("/")) {
|
|
||||||
endpoint = endpoint.substring(1);
|
public void disableEndpoint(String endpoint) {
|
||||||
}
|
if (!endpointStatuses.containsKey(endpoint) || endpointStatuses.get(endpoint) != false) {
|
||||||
return endpointStatuses.getOrDefault(endpoint, true);
|
logger.info("Disabling {}", endpoint);
|
||||||
}
|
endpointStatuses.put(endpoint, false);
|
||||||
|
}
|
||||||
public void addEndpointToGroup(String group, String endpoint) {
|
}
|
||||||
endpointGroups.computeIfAbsent(group, k -> new HashSet<>()).add(endpoint);
|
|
||||||
}
|
public boolean isEndpointEnabled(String endpoint) {
|
||||||
|
if (endpoint.startsWith("/")) {
|
||||||
public void enableGroup(String group) {
|
endpoint = endpoint.substring(1);
|
||||||
Set<String> endpoints = endpointGroups.get(group);
|
}
|
||||||
if (endpoints != null) {
|
return endpointStatuses.getOrDefault(endpoint, true);
|
||||||
for (String endpoint : endpoints) {
|
}
|
||||||
enableEndpoint(endpoint);
|
|
||||||
}
|
public void addEndpointToGroup(String group, String endpoint) {
|
||||||
}
|
endpointGroups.computeIfAbsent(group, k -> new HashSet<>()).add(endpoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void disableGroup(String group) {
|
public void enableGroup(String group) {
|
||||||
Set<String> endpoints = endpointGroups.get(group);
|
Set<String> endpoints = endpointGroups.get(group);
|
||||||
if (endpoints != null) {
|
if (endpoints != null) {
|
||||||
for (String endpoint : endpoints) {
|
for (String endpoint : endpoints) {
|
||||||
disableEndpoint(endpoint);
|
enableEndpoint(endpoint);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void init() {
|
public void disableGroup(String group) {
|
||||||
// Adding endpoints to "PageOps" group
|
Set<String> endpoints = endpointGroups.get(group);
|
||||||
addEndpointToGroup("PageOps", "remove-pages");
|
if (endpoints != null) {
|
||||||
addEndpointToGroup("PageOps", "merge-pdfs");
|
for (String endpoint : endpoints) {
|
||||||
addEndpointToGroup("PageOps", "split-pdfs");
|
disableEndpoint(endpoint);
|
||||||
addEndpointToGroup("PageOps", "pdf-organizer");
|
}
|
||||||
addEndpointToGroup("PageOps", "rotate-pdf");
|
}
|
||||||
addEndpointToGroup("PageOps", "multi-page-layout");
|
}
|
||||||
addEndpointToGroup("PageOps", "scale-pages");
|
|
||||||
addEndpointToGroup("PageOps", "adjust-contrast");
|
public void init() {
|
||||||
addEndpointToGroup("PageOps", "crop");
|
// Adding endpoints to "PageOps" group
|
||||||
addEndpointToGroup("PageOps", "auto-split-pdf");
|
addEndpointToGroup("PageOps", "remove-pages");
|
||||||
addEndpointToGroup("PageOps", "extract-page");
|
addEndpointToGroup("PageOps", "merge-pdfs");
|
||||||
addEndpointToGroup("PageOps", "pdf-to-single-page");
|
addEndpointToGroup("PageOps", "split-pdfs");
|
||||||
|
addEndpointToGroup("PageOps", "pdf-organizer");
|
||||||
// Adding endpoints to "Convert" group
|
addEndpointToGroup("PageOps", "rotate-pdf");
|
||||||
addEndpointToGroup("Convert", "pdf-to-img");
|
addEndpointToGroup("PageOps", "multi-page-layout");
|
||||||
addEndpointToGroup("Convert", "img-to-pdf");
|
addEndpointToGroup("PageOps", "scale-pages");
|
||||||
addEndpointToGroup("Convert", "pdf-to-pdfa");
|
addEndpointToGroup("PageOps", "adjust-contrast");
|
||||||
addEndpointToGroup("Convert", "file-to-pdf");
|
addEndpointToGroup("PageOps", "crop");
|
||||||
addEndpointToGroup("Convert", "xlsx-to-pdf");
|
addEndpointToGroup("PageOps", "auto-split-pdf");
|
||||||
addEndpointToGroup("Convert", "pdf-to-word");
|
addEndpointToGroup("PageOps", "extract-page");
|
||||||
addEndpointToGroup("Convert", "pdf-to-presentation");
|
addEndpointToGroup("PageOps", "pdf-to-single-page");
|
||||||
addEndpointToGroup("Convert", "pdf-to-text");
|
addEndpointToGroup("PageOps", "split-by-size-or-count");
|
||||||
addEndpointToGroup("Convert", "pdf-to-html");
|
addEndpointToGroup("PageOps", "overlay-pdf");
|
||||||
addEndpointToGroup("Convert", "pdf-to-xml");
|
addEndpointToGroup("PageOps", "split-pdf-by-sections");
|
||||||
addEndpointToGroup("Convert", "html-to-pdf");
|
|
||||||
addEndpointToGroup("Convert", "url-to-pdf");
|
// Adding endpoints to "Convert" group
|
||||||
addEndpointToGroup("Convert", "markdown-to-pdf");
|
addEndpointToGroup("Convert", "pdf-to-img");
|
||||||
|
addEndpointToGroup("Convert", "img-to-pdf");
|
||||||
// Adding endpoints to "Security" group
|
addEndpointToGroup("Convert", "pdf-to-pdfa");
|
||||||
addEndpointToGroup("Security", "add-password");
|
addEndpointToGroup("Convert", "file-to-pdf");
|
||||||
addEndpointToGroup("Security", "remove-password");
|
addEndpointToGroup("Convert", "xlsx-to-pdf");
|
||||||
addEndpointToGroup("Security", "change-permissions");
|
addEndpointToGroup("Convert", "pdf-to-word");
|
||||||
addEndpointToGroup("Security", "add-watermark");
|
addEndpointToGroup("Convert", "pdf-to-presentation");
|
||||||
addEndpointToGroup("Security", "cert-sign");
|
addEndpointToGroup("Convert", "pdf-to-text");
|
||||||
addEndpointToGroup("Security", "sanitize-pdf");
|
addEndpointToGroup("Convert", "pdf-to-html");
|
||||||
|
addEndpointToGroup("Convert", "pdf-to-xml");
|
||||||
|
addEndpointToGroup("Convert", "html-to-pdf");
|
||||||
// Adding endpoints to "Other" group
|
addEndpointToGroup("Convert", "url-to-pdf");
|
||||||
addEndpointToGroup("Other", "ocr-pdf");
|
addEndpointToGroup("Convert", "markdown-to-pdf");
|
||||||
addEndpointToGroup("Other", "add-image");
|
addEndpointToGroup("Convert", "pdf-to-csv");
|
||||||
addEndpointToGroup("Other", "compress-pdf");
|
|
||||||
addEndpointToGroup("Other", "extract-images");
|
// Adding endpoints to "Security" group
|
||||||
addEndpointToGroup("Other", "change-metadata");
|
addEndpointToGroup("Security", "add-password");
|
||||||
addEndpointToGroup("Other", "extract-image-scans");
|
addEndpointToGroup("Security", "remove-password");
|
||||||
addEndpointToGroup("Other", "sign");
|
addEndpointToGroup("Security", "change-permissions");
|
||||||
addEndpointToGroup("Other", "flatten");
|
addEndpointToGroup("Security", "add-watermark");
|
||||||
addEndpointToGroup("Other", "repair");
|
addEndpointToGroup("Security", "cert-sign");
|
||||||
addEndpointToGroup("Other", "remove-blanks");
|
addEndpointToGroup("Security", "sanitize-pdf");
|
||||||
addEndpointToGroup("Other", "compare");
|
addEndpointToGroup("Security", "auto-redact");
|
||||||
addEndpointToGroup("Other", "add-page-numbers");
|
|
||||||
addEndpointToGroup("Other", "auto-rename");
|
// Adding endpoints to "Other" group
|
||||||
addEndpointToGroup("Other", "get-info-on-pdf");
|
addEndpointToGroup("Other", "ocr-pdf");
|
||||||
addEndpointToGroup("Other", "show-javascript");
|
addEndpointToGroup("Other", "add-image");
|
||||||
|
addEndpointToGroup("Other", "compress-pdf");
|
||||||
|
addEndpointToGroup("Other", "extract-images");
|
||||||
|
addEndpointToGroup("Other", "change-metadata");
|
||||||
//CLI
|
addEndpointToGroup("Other", "extract-image-scans");
|
||||||
addEndpointToGroup("CLI", "compress-pdf");
|
addEndpointToGroup("Other", "sign");
|
||||||
addEndpointToGroup("CLI", "extract-image-scans");
|
addEndpointToGroup("Other", "flatten");
|
||||||
addEndpointToGroup("CLI", "remove-blanks");
|
addEndpointToGroup("Other", "repair");
|
||||||
addEndpointToGroup("CLI", "repair");
|
addEndpointToGroup("Other", "remove-blanks");
|
||||||
addEndpointToGroup("CLI", "pdf-to-pdfa");
|
addEndpointToGroup("Other", "remove-annotations");
|
||||||
addEndpointToGroup("CLI", "file-to-pdf");
|
addEndpointToGroup("Other", "compare");
|
||||||
addEndpointToGroup("CLI", "xlsx-to-pdf");
|
addEndpointToGroup("Other", "add-page-numbers");
|
||||||
addEndpointToGroup("CLI", "pdf-to-word");
|
addEndpointToGroup("Other", "auto-rename");
|
||||||
addEndpointToGroup("CLI", "pdf-to-presentation");
|
addEndpointToGroup("Other", "get-info-on-pdf");
|
||||||
addEndpointToGroup("CLI", "pdf-to-text");
|
addEndpointToGroup("Other", "show-javascript");
|
||||||
addEndpointToGroup("CLI", "pdf-to-html");
|
|
||||||
addEndpointToGroup("CLI", "pdf-to-xml");
|
// CLI
|
||||||
addEndpointToGroup("CLI", "ocr-pdf");
|
addEndpointToGroup("CLI", "compress-pdf");
|
||||||
addEndpointToGroup("CLI", "html-to-pdf");
|
addEndpointToGroup("CLI", "extract-image-scans");
|
||||||
addEndpointToGroup("CLI", "url-to-pdf");
|
addEndpointToGroup("CLI", "remove-blanks");
|
||||||
|
addEndpointToGroup("CLI", "repair");
|
||||||
|
addEndpointToGroup("CLI", "pdf-to-pdfa");
|
||||||
//python
|
addEndpointToGroup("CLI", "file-to-pdf");
|
||||||
addEndpointToGroup("Python", "extract-image-scans");
|
addEndpointToGroup("CLI", "xlsx-to-pdf");
|
||||||
addEndpointToGroup("Python", "remove-blanks");
|
addEndpointToGroup("CLI", "pdf-to-word");
|
||||||
addEndpointToGroup("Python", "html-to-pdf");
|
addEndpointToGroup("CLI", "pdf-to-presentation");
|
||||||
addEndpointToGroup("Python", "url-to-pdf");
|
addEndpointToGroup("CLI", "pdf-to-text");
|
||||||
|
addEndpointToGroup("CLI", "pdf-to-html");
|
||||||
//openCV
|
addEndpointToGroup("CLI", "pdf-to-xml");
|
||||||
addEndpointToGroup("OpenCV", "extract-image-scans");
|
addEndpointToGroup("CLI", "ocr-pdf");
|
||||||
addEndpointToGroup("OpenCV", "remove-blanks");
|
addEndpointToGroup("CLI", "html-to-pdf");
|
||||||
|
addEndpointToGroup("CLI", "url-to-pdf");
|
||||||
//LibreOffice
|
addEndpointToGroup("CLI", "book-to-pdf");
|
||||||
addEndpointToGroup("LibreOffice", "repair");
|
addEndpointToGroup("CLI", "pdf-to-book");
|
||||||
addEndpointToGroup("LibreOffice", "file-to-pdf");
|
|
||||||
addEndpointToGroup("LibreOffice", "xlsx-to-pdf");
|
// Calibre
|
||||||
addEndpointToGroup("LibreOffice", "pdf-to-word");
|
addEndpointToGroup("Calibre", "book-to-pdf");
|
||||||
addEndpointToGroup("LibreOffice", "pdf-to-presentation");
|
addEndpointToGroup("Calibre", "pdf-to-book");
|
||||||
addEndpointToGroup("LibreOffice", "pdf-to-text");
|
|
||||||
addEndpointToGroup("LibreOffice", "pdf-to-html");
|
// python
|
||||||
addEndpointToGroup("LibreOffice", "pdf-to-xml");
|
addEndpointToGroup("Python", "extract-image-scans");
|
||||||
|
addEndpointToGroup("Python", "remove-blanks");
|
||||||
|
addEndpointToGroup("Python", "html-to-pdf");
|
||||||
//OCRmyPDF
|
addEndpointToGroup("Python", "url-to-pdf");
|
||||||
addEndpointToGroup("OCRmyPDF", "compress-pdf");
|
|
||||||
addEndpointToGroup("OCRmyPDF", "pdf-to-pdfa");
|
// openCV
|
||||||
addEndpointToGroup("OCRmyPDF", "ocr-pdf");
|
addEndpointToGroup("OpenCV", "extract-image-scans");
|
||||||
|
addEndpointToGroup("OpenCV", "remove-blanks");
|
||||||
//Java
|
|
||||||
addEndpointToGroup("Java", "merge-pdfs");
|
// LibreOffice
|
||||||
addEndpointToGroup("Java", "remove-pages");
|
addEndpointToGroup("LibreOffice", "repair");
|
||||||
addEndpointToGroup("Java", "split-pdfs");
|
addEndpointToGroup("LibreOffice", "file-to-pdf");
|
||||||
addEndpointToGroup("Java", "pdf-organizer");
|
addEndpointToGroup("LibreOffice", "xlsx-to-pdf");
|
||||||
addEndpointToGroup("Java", "rotate-pdf");
|
addEndpointToGroup("LibreOffice", "pdf-to-word");
|
||||||
addEndpointToGroup("Java", "pdf-to-img");
|
addEndpointToGroup("LibreOffice", "pdf-to-presentation");
|
||||||
addEndpointToGroup("Java", "img-to-pdf");
|
addEndpointToGroup("LibreOffice", "pdf-to-text");
|
||||||
addEndpointToGroup("Java", "add-password");
|
addEndpointToGroup("LibreOffice", "pdf-to-html");
|
||||||
addEndpointToGroup("Java", "remove-password");
|
addEndpointToGroup("LibreOffice", "pdf-to-xml");
|
||||||
addEndpointToGroup("Java", "change-permissions");
|
|
||||||
addEndpointToGroup("Java", "add-watermark");
|
// OCRmyPDF
|
||||||
addEndpointToGroup("Java", "add-image");
|
addEndpointToGroup("OCRmyPDF", "compress-pdf");
|
||||||
addEndpointToGroup("Java", "extract-images");
|
addEndpointToGroup("OCRmyPDF", "pdf-to-pdfa");
|
||||||
addEndpointToGroup("Java", "change-metadata");
|
addEndpointToGroup("OCRmyPDF", "ocr-pdf");
|
||||||
addEndpointToGroup("Java", "cert-sign");
|
|
||||||
addEndpointToGroup("Java", "multi-page-layout");
|
// Java
|
||||||
addEndpointToGroup("Java", "scale-pages");
|
addEndpointToGroup("Java", "merge-pdfs");
|
||||||
addEndpointToGroup("Java", "add-page-numbers");
|
addEndpointToGroup("Java", "remove-pages");
|
||||||
addEndpointToGroup("Java", "auto-rename");
|
addEndpointToGroup("Java", "split-pdfs");
|
||||||
addEndpointToGroup("Java", "auto-split-pdf");
|
addEndpointToGroup("Java", "pdf-organizer");
|
||||||
addEndpointToGroup("Java", "sanitize-pdf");
|
addEndpointToGroup("Java", "rotate-pdf");
|
||||||
addEndpointToGroup("Java", "crop");
|
addEndpointToGroup("Java", "pdf-to-img");
|
||||||
addEndpointToGroup("Java", "get-info-on-pdf");
|
addEndpointToGroup("Java", "img-to-pdf");
|
||||||
addEndpointToGroup("Java", "extract-page");
|
addEndpointToGroup("Java", "add-password");
|
||||||
addEndpointToGroup("Java", "pdf-to-single-page");
|
addEndpointToGroup("Java", "remove-password");
|
||||||
addEndpointToGroup("Java", "markdown-to-pdf");
|
addEndpointToGroup("Java", "change-permissions");
|
||||||
addEndpointToGroup("Java", "show-javascript");
|
addEndpointToGroup("Java", "add-watermark");
|
||||||
|
addEndpointToGroup("Java", "add-image");
|
||||||
//Javascript
|
addEndpointToGroup("Java", "extract-images");
|
||||||
addEndpointToGroup("Javascript", "pdf-organizer");
|
addEndpointToGroup("Java", "change-metadata");
|
||||||
addEndpointToGroup("Javascript", "sign");
|
addEndpointToGroup("Java", "cert-sign");
|
||||||
addEndpointToGroup("Javascript", "compare");
|
addEndpointToGroup("Java", "multi-page-layout");
|
||||||
addEndpointToGroup("Javascript", "adjust-contrast");
|
addEndpointToGroup("Java", "scale-pages");
|
||||||
|
addEndpointToGroup("Java", "add-page-numbers");
|
||||||
|
addEndpointToGroup("Java", "auto-rename");
|
||||||
}
|
addEndpointToGroup("Java", "auto-split-pdf");
|
||||||
|
addEndpointToGroup("Java", "sanitize-pdf");
|
||||||
private void processEnvironmentConfigs() {
|
addEndpointToGroup("Java", "crop");
|
||||||
List<String> endpointsToRemove = applicationProperties.getEndpoints().getToRemove();
|
addEndpointToGroup("Java", "get-info-on-pdf");
|
||||||
List<String> groupsToRemove = applicationProperties.getEndpoints().getGroupsToRemove();
|
addEndpointToGroup("Java", "extract-page");
|
||||||
|
addEndpointToGroup("Java", "pdf-to-single-page");
|
||||||
if (endpointsToRemove != null) {
|
addEndpointToGroup("Java", "markdown-to-pdf");
|
||||||
for (String endpoint : endpointsToRemove) {
|
addEndpointToGroup("Java", "show-javascript");
|
||||||
disableEndpoint(endpoint.trim());
|
addEndpointToGroup("Java", "auto-redact");
|
||||||
}
|
addEndpointToGroup("Java", "pdf-to-csv");
|
||||||
}
|
addEndpointToGroup("Java", "split-by-size-or-count");
|
||||||
|
addEndpointToGroup("Java", "overlay-pdf");
|
||||||
if (groupsToRemove != null) {
|
addEndpointToGroup("Java", "split-pdf-by-sections");
|
||||||
for (String group : groupsToRemove) {
|
|
||||||
disableGroup(group.trim());
|
// Javascript
|
||||||
}
|
addEndpointToGroup("Javascript", "pdf-organizer");
|
||||||
}
|
addEndpointToGroup("Javascript", "sign");
|
||||||
}
|
addEndpointToGroup("Javascript", "compare");
|
||||||
|
addEndpointToGroup("Javascript", "adjust-contrast");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void processEnvironmentConfigs() {
|
||||||
|
List<String> endpointsToRemove = applicationProperties.getEndpoints().getToRemove();
|
||||||
|
List<String> groupsToRemove = applicationProperties.getEndpoints().getGroupsToRemove();
|
||||||
|
if (!bookAndHtmlFormatsInstalled) {
|
||||||
|
groupsToRemove.add("Calibre");
|
||||||
|
}
|
||||||
|
if (endpointsToRemove != null) {
|
||||||
|
for (String endpoint : endpointsToRemove) {
|
||||||
|
disableEndpoint(endpoint.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (groupsToRemove != null) {
|
||||||
|
for (String group : groupsToRemove) {
|
||||||
|
disableGroup(group.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,26 +1,26 @@
|
|||||||
package stirling.software.SPDF.config;
|
package stirling.software.SPDF.config;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.web.servlet.HandlerInterceptor;
|
import org.springframework.web.servlet.HandlerInterceptor;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
public class EndpointInterceptor implements HandlerInterceptor {
|
public class EndpointInterceptor implements HandlerInterceptor {
|
||||||
|
|
||||||
@Autowired
|
@Autowired private EndpointConfiguration endpointConfiguration;
|
||||||
private EndpointConfiguration endpointConfiguration;
|
|
||||||
|
@Override
|
||||||
@Override
|
public boolean preHandle(
|
||||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
|
HttpServletRequest request, HttpServletResponse response, Object handler)
|
||||||
throws Exception {
|
throws Exception {
|
||||||
String requestURI = request.getRequestURI();
|
String requestURI = request.getRequestURI();
|
||||||
if (!endpointConfiguration.isEndpointEnabled(requestURI)) {
|
if (!endpointConfiguration.isEndpointEnabled(requestURI)) {
|
||||||
response.sendError(HttpServletResponse.SC_FORBIDDEN, "This endpoint is disabled");
|
response.sendError(HttpServletResponse.SC_FORBIDDEN, "This endpoint is disabled");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,25 @@
|
|||||||
package stirling.software.SPDF.config;
|
package stirling.software.SPDF.config;
|
||||||
import org.springframework.context.annotation.Bean;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
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.Meter;
|
||||||
import io.micrometer.core.instrument.config.MeterFilterReply;
|
import io.micrometer.core.instrument.config.MeterFilter;
|
||||||
|
import io.micrometer.core.instrument.config.MeterFilterReply;
|
||||||
@Configuration
|
|
||||||
public class MetricsConfig {
|
@Configuration
|
||||||
|
public class MetricsConfig {
|
||||||
@Bean
|
|
||||||
public MeterFilter meterFilter() {
|
@Bean
|
||||||
return new MeterFilter() {
|
public MeterFilter meterFilter() {
|
||||||
@Override
|
return new MeterFilter() {
|
||||||
public MeterFilterReply accept(Meter.Id id) {
|
@Override
|
||||||
if (id.getName().equals("http.requests")) {
|
public MeterFilterReply accept(Meter.Id id) {
|
||||||
return MeterFilterReply.NEUTRAL;
|
if (id.getName().equals("http.requests")) {
|
||||||
}
|
return MeterFilterReply.NEUTRAL;
|
||||||
return MeterFilterReply.DENY;
|
}
|
||||||
}
|
return MeterFilterReply.DENY;
|
||||||
};
|
}
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,48 +1,64 @@
|
|||||||
package stirling.software.SPDF.config;
|
package stirling.software.SPDF.config;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.web.filter.OncePerRequestFilter;
|
import org.springframework.web.filter.OncePerRequestFilter;
|
||||||
|
|
||||||
import io.micrometer.core.instrument.Counter;
|
import io.micrometer.core.instrument.Counter;
|
||||||
import io.micrometer.core.instrument.MeterRegistry;
|
import io.micrometer.core.instrument.MeterRegistry;
|
||||||
import jakarta.servlet.FilterChain;
|
|
||||||
import jakarta.servlet.ServletException;
|
import jakarta.servlet.FilterChain;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.ServletException;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
@Component
|
|
||||||
public class MetricsFilter extends OncePerRequestFilter {
|
@Component
|
||||||
|
public class MetricsFilter extends OncePerRequestFilter {
|
||||||
private final MeterRegistry meterRegistry;
|
|
||||||
|
private final MeterRegistry meterRegistry;
|
||||||
@Autowired
|
|
||||||
public MetricsFilter(MeterRegistry meterRegistry) {
|
@Autowired
|
||||||
this.meterRegistry = meterRegistry;
|
public MetricsFilter(MeterRegistry meterRegistry) {
|
||||||
}
|
this.meterRegistry = meterRegistry;
|
||||||
|
}
|
||||||
@Override
|
|
||||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
@Override
|
||||||
throws ServletException, IOException {
|
protected void doFilterInternal(
|
||||||
String uri = request.getRequestURI();
|
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||||
|
throws ServletException, IOException {
|
||||||
//System.out.println("uri="+uri + ", method=" + request.getMethod() );
|
String uri = request.getRequestURI();
|
||||||
// Ignore static resources
|
|
||||||
if (!(uri.startsWith("/js") || uri.startsWith("api-docs") || uri.endsWith("robots.txt") || uri.startsWith("/images") || uri.endsWith(".png") || uri.endsWith(".ico") || uri.endsWith(".css") || uri.endsWith(".svg")|| uri.endsWith(".js") || uri.contains("swagger") || uri.startsWith("/api"))) {
|
// System.out.println("uri="+uri + ", method=" + request.getMethod() );
|
||||||
Counter counter = Counter.builder("http.requests")
|
// Ignore static resources
|
||||||
.tag("uri", uri)
|
if (!(uri.startsWith("/js")
|
||||||
.tag("method", request.getMethod())
|
|| uri.startsWith("/v1/api-docs")
|
||||||
.register(meterRegistry);
|
|| uri.endsWith("robots.txt")
|
||||||
|
|| uri.startsWith("/images")
|
||||||
counter.increment();
|
|| uri.startsWith("/images")
|
||||||
//System.out.println("Counted");
|
|| uri.endsWith(".png")
|
||||||
}
|
|| uri.endsWith(".ico")
|
||||||
|
|| uri.endsWith(".css")
|
||||||
filterChain.doFilter(request, response);
|
|| 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,27 +1,53 @@
|
|||||||
package stirling.software.SPDF.config;
|
package stirling.software.SPDF.config;
|
||||||
|
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.context.annotation.Configuration;
|
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.Components;
|
||||||
import io.swagger.v3.oas.models.info.Info;
|
import io.swagger.v3.oas.models.OpenAPI;
|
||||||
|
import io.swagger.v3.oas.models.info.Info;
|
||||||
@Configuration
|
import io.swagger.v3.oas.models.security.SecurityRequirement;
|
||||||
public class OpenApiConfig {
|
import io.swagger.v3.oas.models.security.SecurityScheme;
|
||||||
|
|
||||||
@Bean
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
public OpenAPI customOpenAPI() {
|
|
||||||
String version = getClass().getPackage().getImplementationVersion();
|
@Configuration
|
||||||
if (version == null) {
|
public class OpenApiConfig {
|
||||||
|
|
||||||
version = "1.0.0"; // default version if all else fails
|
@Autowired ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
}
|
@Bean
|
||||||
|
public OpenAPI customOpenAPI() {
|
||||||
return new OpenAPI().components(new Components()).info(
|
String version = getClass().getPackage().getImplementationVersion();
|
||||||
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."));
|
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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package stirling.software.SPDF.config;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
|
import stirling.software.SPDF.utils.ProcessExecutor;
|
||||||
|
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class PostStartupProcesses {
|
||||||
|
|
||||||
|
@Autowired ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
@Qualifier("RunningInDocker")
|
||||||
|
private boolean runningInDocker;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
@Qualifier("bookAndHtmlFormatsInstalled")
|
||||||
|
private boolean bookAndHtmlFormatsInstalled;
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(PostStartupProcesses.class);
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void runInstallCommandBasedOnEnvironment() throws IOException, InterruptedException {
|
||||||
|
List<List<String>> commands = new ArrayList<>();
|
||||||
|
// Checking for DOCKER_INSTALL_BOOK_FORMATS environment variable
|
||||||
|
if (bookAndHtmlFormatsInstalled) {
|
||||||
|
List<String> tmpList = new ArrayList<>();
|
||||||
|
|
||||||
|
tmpList = new ArrayList<>();
|
||||||
|
tmpList.addAll(Arrays.asList("apk add --no-cache calibre"));
|
||||||
|
commands.add(tmpList);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!commands.isEmpty()) {
|
||||||
|
// Run the command
|
||||||
|
if (runningInDocker) {
|
||||||
|
List<String> tmpList = new ArrayList<>();
|
||||||
|
|
||||||
|
for (List<String> list : commands) {
|
||||||
|
ProcessExecutorResult returnCode =
|
||||||
|
ProcessExecutor.getInstance(ProcessExecutor.Processes.INSTALL_APP, true)
|
||||||
|
.runCommandWithOutputHandling(list);
|
||||||
|
logger.info("RC for app installs {}", returnCode.getRc());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"Not running inside Docker so skipping automated install process with command.");
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
if (runningInDocker) {
|
||||||
|
logger.info("No custom apps to install.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,20 +1,18 @@
|
|||||||
package stirling.software.SPDF.config;
|
package stirling.software.SPDF.config;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
import java.time.LocalDateTime;
|
|
||||||
|
import org.springframework.context.ApplicationListener;
|
||||||
import org.springframework.context.ApplicationListener;
|
import org.springframework.context.event.ContextRefreshedEvent;
|
||||||
import org.springframework.context.event.ContextRefreshedEvent;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
@Component
|
||||||
@Component
|
public class StartupApplicationListener implements ApplicationListener<ContextRefreshedEvent> {
|
||||||
public class StartupApplicationListener implements ApplicationListener<ContextRefreshedEvent> {
|
|
||||||
|
public static LocalDateTime startTime;
|
||||||
public static LocalDateTime startTime;
|
|
||||||
|
@Override
|
||||||
@Override
|
public void onApplicationEvent(ContextRefreshedEvent event) {
|
||||||
public void onApplicationEvent(ContextRefreshedEvent event) {
|
startTime = LocalDateTime.now();
|
||||||
startTime = LocalDateTime.now();
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,27 +1,26 @@
|
|||||||
package stirling.software.SPDF.config;
|
package stirling.software.SPDF.config;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||||
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
public class WebMvcConfig implements WebMvcConfigurer {
|
public class WebMvcConfig implements WebMvcConfigurer {
|
||||||
|
|
||||||
@Autowired
|
@Autowired private EndpointInterceptor endpointInterceptor;
|
||||||
private EndpointInterceptor endpointInterceptor;
|
|
||||||
|
@Override
|
||||||
@Override
|
public void addInterceptors(InterceptorRegistry registry) {
|
||||||
public void addInterceptors(InterceptorRegistry registry) {
|
registry.addInterceptor(endpointInterceptor);
|
||||||
registry.addInterceptor(endpointInterceptor);
|
}
|
||||||
}
|
|
||||||
|
@Override
|
||||||
@Override
|
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||||
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
// Handler for external static resources
|
||||||
// Handler for external static resources
|
registry.addResourceHandler("/**")
|
||||||
registry.addResourceHandler("/**")
|
.addResourceLocations("file:customFiles/static/", "classpath:/static/");
|
||||||
.addResourceLocations("file:customFiles/static/", "classpath:/static/");
|
// .setCachePeriod(0); // Optional: disable caching
|
||||||
//.setCachePeriod(0); // Optional: disable caching
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -8,16 +8,18 @@ import org.springframework.core.env.PropertiesPropertySource;
|
|||||||
import org.springframework.core.env.PropertySource;
|
import org.springframework.core.env.PropertySource;
|
||||||
import org.springframework.core.io.support.EncodedResource;
|
import org.springframework.core.io.support.EncodedResource;
|
||||||
import org.springframework.core.io.support.PropertySourceFactory;
|
import org.springframework.core.io.support.PropertySourceFactory;
|
||||||
|
|
||||||
public class YamlPropertySourceFactory implements PropertySourceFactory {
|
public class YamlPropertySourceFactory implements PropertySourceFactory {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PropertySource<?> createPropertySource(String name, EncodedResource encodedResource)
|
public PropertySource<?> createPropertySource(String name, EncodedResource encodedResource)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
|
YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
|
||||||
factory.setResources(encodedResource.getResource());
|
factory.setResources(encodedResource.getResource());
|
||||||
|
|
||||||
Properties properties = factory.getObject();
|
Properties properties = factory.getObject();
|
||||||
|
|
||||||
return new PropertiesPropertySource(encodedResource.getResource().getFilename(), properties);
|
return new PropertiesPropertySource(
|
||||||
|
encodedResource.getResource().getFilename(), properties);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,49 @@
|
|||||||
package stirling.software.SPDF.config.security;
|
package stirling.software.SPDF.config.security;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
import org.springframework.security.authentication.BadCredentialsException;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.security.authentication.LockedException;
|
import org.springframework.security.authentication.BadCredentialsException;
|
||||||
import org.springframework.security.core.AuthenticationException;
|
import org.springframework.security.authentication.LockedException;
|
||||||
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
|
import org.springframework.security.core.AuthenticationException;
|
||||||
|
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
|
||||||
import jakarta.servlet.ServletException;
|
import org.springframework.stereotype.Component;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
@Override
|
@Component
|
||||||
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception)
|
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
|
||||||
throws IOException, ServletException {
|
|
||||||
if (exception.getClass().isAssignableFrom(BadCredentialsException.class)) {
|
@Autowired private final LoginAttemptService loginAttemptService;
|
||||||
setDefaultFailureUrl("/login?error=badcredentials");
|
|
||||||
} else if (exception.getClass().isAssignableFrom(LockedException.class)) {
|
@Autowired
|
||||||
setDefaultFailureUrl("/login?error=locked");
|
public CustomAuthenticationFailureHandler(LoginAttemptService loginAttemptService) {
|
||||||
}
|
this.loginAttemptService = loginAttemptService;
|
||||||
super.onAuthenticationFailure(request, response, exception);
|
}
|
||||||
}
|
|
||||||
}
|
@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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,45 +1,57 @@
|
|||||||
package stirling.software.SPDF.config.security;
|
package stirling.software.SPDF.config.security;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.security.core.GrantedAuthority;
|
import org.springframework.security.authentication.LockedException;
|
||||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||||
import org.springframework.stereotype.Service;
|
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.model.Authority;
|
||||||
import stirling.software.SPDF.repository.UserRepository;
|
import stirling.software.SPDF.model.User;
|
||||||
|
import stirling.software.SPDF.repository.UserRepository;
|
||||||
@Service
|
|
||||||
public class CustomUserDetailsService implements UserDetailsService {
|
@Service
|
||||||
|
public class CustomUserDetailsService implements UserDetailsService {
|
||||||
@Autowired
|
|
||||||
private UserRepository userRepository;
|
@Autowired private UserRepository userRepository;
|
||||||
|
|
||||||
|
@Autowired private LoginAttemptService loginAttemptService;
|
||||||
@Override
|
|
||||||
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
|
@Override
|
||||||
User user = userRepository.findByUsername(username)
|
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
|
||||||
.orElseThrow(() -> new UsernameNotFoundException("No user found with username: " + username));
|
User user =
|
||||||
|
userRepository
|
||||||
return new org.springframework.security.core.userdetails.User(
|
.findByUsername(username)
|
||||||
user.getUsername(),
|
.orElseThrow(
|
||||||
user.getPassword(),
|
() ->
|
||||||
user.isEnabled(),
|
new UsernameNotFoundException(
|
||||||
true, true, true,
|
"No user found with username: " + username));
|
||||||
getAuthorities(user.getAuthorities())
|
|
||||||
);
|
if (loginAttemptService.isBlocked(username)) {
|
||||||
}
|
throw new LockedException(
|
||||||
|
"Your account has been locked due to too many failed login attempts.");
|
||||||
private Collection<? extends GrantedAuthority> getAuthorities(Set<Authority> authorities) {
|
}
|
||||||
return authorities.stream()
|
|
||||||
.map(authority -> new SimpleGrantedAuthority(authority.getAuthority()))
|
return new org.springframework.security.core.userdetails.User(
|
||||||
.collect(Collectors.toList());
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,35 +15,35 @@ import jakarta.servlet.ServletException;
|
|||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import stirling.software.SPDF.model.User;
|
import stirling.software.SPDF.model.User;
|
||||||
|
import stirling.software.SPDF.utils.RequestUriUtils;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
public class FirstLoginFilter extends OncePerRequestFilter {
|
public class FirstLoginFilter extends OncePerRequestFilter {
|
||||||
|
|
||||||
@Autowired
|
@Autowired @Lazy private UserService userService;
|
||||||
@Lazy
|
|
||||||
private UserService userService;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
|
protected void doFilterInternal(
|
||||||
String method = request.getMethod();
|
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||||
String requestURI = request.getRequestURI();
|
throws ServletException, IOException {
|
||||||
// Check if the request is for static resources
|
String method = request.getMethod();
|
||||||
boolean isStaticResource = requestURI.startsWith("/css/")
|
String requestURI = request.getRequestURI();
|
||||||
|| requestURI.startsWith("/js/")
|
// Check if the request is for static resources
|
||||||
|| requestURI.startsWith("/images/")
|
boolean isStaticResource = RequestUriUtils.isStaticResource(requestURI);
|
||||||
|| requestURI.startsWith("/public/")
|
|
||||||
|| requestURI.endsWith(".svg");
|
|
||||||
|
|
||||||
// If it's a static resource, just continue the filter chain and skip the logic below
|
// If it's a static resource, just continue the filter chain and skip the logic below
|
||||||
if (isStaticResource) {
|
if (isStaticResource) {
|
||||||
filterChain.doFilter(request, response);
|
filterChain.doFilter(request, response);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||||
if (authentication != null && authentication.isAuthenticated()) {
|
if (authentication != null && authentication.isAuthenticated()) {
|
||||||
Optional<User> user = userService.findByUsername(authentication.getName());
|
Optional<User> user = userService.findByUsername(authentication.getName());
|
||||||
if ("GET".equalsIgnoreCase(method) && user.isPresent() && user.get().isFirstLogin() && !"/change-creds".equals(requestURI)) {
|
if ("GET".equalsIgnoreCase(method)
|
||||||
|
&& user.isPresent()
|
||||||
|
&& user.get().isFirstLogin()
|
||||||
|
&& !"/change-creds".equals(requestURI)) {
|
||||||
response.sendRedirect("/change-creds");
|
response.sendRedirect("/change-creds");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,66 +13,76 @@ import org.springframework.stereotype.Component;
|
|||||||
import jakarta.annotation.PostConstruct;
|
import jakarta.annotation.PostConstruct;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
import stirling.software.SPDF.model.Role;
|
import stirling.software.SPDF.model.Role;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
public class InitialSecuritySetup {
|
public class InitialSecuritySetup {
|
||||||
|
|
||||||
@Autowired
|
@Autowired private UserService userService;
|
||||||
private UserService userService;
|
|
||||||
|
|
||||||
|
@Autowired ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
@Autowired
|
@PostConstruct
|
||||||
ApplicationProperties applicationProperties;
|
public void init() {
|
||||||
|
if (!userService.hasUsers()) {
|
||||||
@PostConstruct
|
|
||||||
public void init() {
|
|
||||||
if (!userService.hasUsers()) {
|
|
||||||
String initialUsername = "admin";
|
|
||||||
String initialPassword = "stirling";
|
|
||||||
userService.saveUser(initialUsername, initialPassword, Role.ADMIN.getRoleId(), true);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@PostConstruct
|
private void saveKeyToConfig(String key) throws IOException {
|
||||||
public void initSecretKey() throws IOException {
|
Path path = Paths.get("configs", "settings.yml"); // Target the configs/settings.yml
|
||||||
String secretKey = applicationProperties.getAutomaticallyGenerated().getKey();
|
List<String> lines = Files.readAllLines(path);
|
||||||
if (secretKey == null || secretKey.isEmpty()) {
|
boolean keyFound = false;
|
||||||
secretKey = UUID.randomUUID().toString(); // Generating a random UUID as the secret key
|
|
||||||
saveKeyToConfig(secretKey);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void saveKeyToConfig(String key) throws IOException {
|
// Search for the existing key to replace it or place to add it
|
||||||
Path path = Paths.get("configs", "settings.yml"); // Target the configs/settings.yml
|
for (int i = 0; i < lines.size(); i++) {
|
||||||
List<String> lines = Files.readAllLines(path);
|
if (lines.get(i).startsWith("AutomaticallyGenerated:")) {
|
||||||
boolean keyFound = false;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Search for the existing key to replace it or place to add it
|
// If the section doesn't exist, append it
|
||||||
for (int i = 0; i < lines.size(); i++) {
|
if (!keyFound) {
|
||||||
if (lines.get(i).startsWith("AutomaticallyGenerated:")) {
|
lines.add("# Automatically Generated Settings (Do Not Edit Directly)");
|
||||||
keyFound = true;
|
lines.add("AutomaticallyGenerated:");
|
||||||
if (i + 1 < lines.size() && lines.get(i + 1).trim().startsWith("key:")) {
|
lines.add(" key: " + key);
|
||||||
lines.set(i + 1, " key: " + key);
|
}
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
lines.add(i + 1, " key: " + key);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the section doesn't exist, append it
|
// Write back to the file
|
||||||
if (!keyFound) {
|
Files.write(path, lines);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,106 +1,140 @@
|
|||||||
package stirling.software.SPDF.config.security;
|
package stirling.software.SPDF.config.security;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.context.annotation.Lazy;
|
import org.springframework.context.annotation.Lazy;
|
||||||
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
|
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
|
||||||
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
|
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.builders.HttpSecurity;
|
||||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.security.web.SecurityFilterChain;
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||||
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
|
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
|
||||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
import org.springframework.security.web.savedrequest.NullRequestCache;
|
||||||
|
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||||
import stirling.software.SPDF.repository.JPATokenRepositoryImpl;
|
|
||||||
@Configuration
|
import stirling.software.SPDF.repository.JPATokenRepositoryImpl;
|
||||||
@EnableWebSecurity()
|
|
||||||
@EnableGlobalMethodSecurity(prePostEnabled = true)
|
@Configuration
|
||||||
public class SecurityConfiguration {
|
@EnableWebSecurity()
|
||||||
|
@EnableMethodSecurity
|
||||||
@Autowired
|
public class SecurityConfiguration {
|
||||||
private UserDetailsService userDetailsService;
|
|
||||||
|
@Autowired private UserDetailsService userDetailsService;
|
||||||
@Bean
|
|
||||||
public PasswordEncoder passwordEncoder() {
|
@Bean
|
||||||
return new BCryptPasswordEncoder();
|
public PasswordEncoder passwordEncoder() {
|
||||||
}
|
return new BCryptPasswordEncoder();
|
||||||
@Autowired
|
}
|
||||||
@Lazy
|
|
||||||
private UserService userService;
|
@Autowired @Lazy private UserService userService;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
@Qualifier("loginEnabled")
|
@Qualifier("loginEnabled")
|
||||||
public boolean loginEnabledValue;
|
public boolean loginEnabledValue;
|
||||||
|
|
||||||
@Autowired
|
@Autowired private UserAuthenticationFilter userAuthenticationFilter;
|
||||||
private UserAuthenticationFilter userAuthenticationFilter;
|
|
||||||
|
@Autowired private LoginAttemptService loginAttemptService;
|
||||||
@Autowired
|
|
||||||
private FirstLoginFilter firstLoginFilter;
|
@Autowired private FirstLoginFilter firstLoginFilter;
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||||
http.addFilterBefore(userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
http.addFilterBefore(userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
||||||
|
|
||||||
if(loginEnabledValue) {
|
if (loginEnabledValue) {
|
||||||
|
|
||||||
http.csrf(csrf -> csrf.disable());
|
http.csrf(csrf -> csrf.disable());
|
||||||
http.addFilterAfter(firstLoginFilter, UsernamePasswordAuthenticationFilter.class);
|
http.addFilterBefore(rateLimitingFilter(), UsernamePasswordAuthenticationFilter.class);
|
||||||
http
|
http.addFilterAfter(firstLoginFilter, UsernamePasswordAuthenticationFilter.class);
|
||||||
.formLogin(formLogin -> formLogin
|
http.formLogin(
|
||||||
.loginPage("/login")
|
formLogin ->
|
||||||
.defaultSuccessUrl("/")
|
formLogin
|
||||||
.failureHandler(new CustomAuthenticationFailureHandler())
|
.loginPage("/login")
|
||||||
.permitAll()
|
.successHandler(
|
||||||
)
|
new CustomAuthenticationSuccessHandler())
|
||||||
.logout(logout -> logout
|
.defaultSuccessUrl("/")
|
||||||
.logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
|
.failureHandler(
|
||||||
.logoutSuccessUrl("/login?logout=true")
|
new CustomAuthenticationFailureHandler(
|
||||||
.invalidateHttpSession(true) // Invalidate session
|
loginAttemptService))
|
||||||
.deleteCookies("JSESSIONID", "remember-me")
|
.permitAll())
|
||||||
).rememberMe(rememberMeConfigurer -> rememberMeConfigurer // Use the configurator directly
|
.requestCache(requestCache -> requestCache.requestCache(new NullRequestCache()))
|
||||||
.key("uniqueAndSecret")
|
.logout(
|
||||||
.tokenRepository(persistentTokenRepository())
|
logout ->
|
||||||
.tokenValiditySeconds(1209600) // 2 weeks
|
logout.logoutRequestMatcher(
|
||||||
)
|
new AntPathRequestMatcher("/logout"))
|
||||||
.authorizeHttpRequests(authz -> authz
|
.logoutSuccessUrl("/login?logout=true")
|
||||||
.requestMatchers(req -> req.getRequestURI().startsWith("/login") || req.getRequestURI().endsWith(".svg") || req.getRequestURI().startsWith("/register") || req.getRequestURI().startsWith("/error") || req.getRequestURI().startsWith("/images/") || req.getRequestURI().startsWith("/public/") || req.getRequestURI().startsWith("/css/") || req.getRequestURI().startsWith("/js/"))
|
.invalidateHttpSession(true) // Invalidate session
|
||||||
.permitAll()
|
.deleteCookies("JSESSIONID", "remember-me"))
|
||||||
.anyRequest().authenticated()
|
.rememberMe(
|
||||||
)
|
rememberMeConfigurer ->
|
||||||
.userDetailsService(userDetailsService)
|
rememberMeConfigurer // Use the configurator directly
|
||||||
.authenticationProvider(authenticationProvider());
|
.key("uniqueAndSecret")
|
||||||
} else {
|
.tokenRepository(persistentTokenRepository())
|
||||||
http.csrf(csrf -> csrf.disable())
|
.tokenValiditySeconds(1209600) // 2 weeks
|
||||||
.authorizeHttpRequests(authz -> authz
|
)
|
||||||
.anyRequest().permitAll()
|
.authorizeHttpRequests(
|
||||||
);
|
authz ->
|
||||||
}
|
authz.requestMatchers(
|
||||||
return http.build();
|
req -> {
|
||||||
}
|
String uri = req.getRequestURI();
|
||||||
|
String contextPath = req.getContextPath();
|
||||||
|
|
||||||
|
// Remove the context path from the URI
|
||||||
@Bean
|
String trimmedUri =
|
||||||
public DaoAuthenticationProvider authenticationProvider() {
|
uri.startsWith(contextPath)
|
||||||
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
|
? uri.substring(
|
||||||
authProvider.setUserDetailsService(userDetailsService);
|
contextPath
|
||||||
authProvider.setPasswordEncoder(passwordEncoder());
|
.length())
|
||||||
return authProvider;
|
: uri;
|
||||||
}
|
|
||||||
|
return trimmedUri.startsWith("/login")
|
||||||
@Bean
|
|| trimmedUri.endsWith(".svg")
|
||||||
public PersistentTokenRepository persistentTokenRepository() {
|
|| trimmedUri.startsWith(
|
||||||
return new JPATokenRepositoryImpl();
|
"/register")
|
||||||
}
|
|| trimmedUri.startsWith("/error")
|
||||||
|
|| trimmedUri.startsWith("/images/")
|
||||||
|
|| trimmedUri.startsWith("/public/")
|
||||||
|
|| trimmedUri.startsWith("/css/")
|
||||||
}
|
|| trimmedUri.startsWith("/js/")
|
||||||
|
|| trimmedUri.startsWith(
|
||||||
|
"/api/v1/info/status");
|
||||||
|
})
|
||||||
|
.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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,113 +1,118 @@
|
|||||||
package stirling.software.SPDF.config.security;
|
package stirling.software.SPDF.config.security;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
import org.springframework.context.annotation.Lazy;
|
import org.springframework.context.annotation.Lazy;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.core.AuthenticationException;
|
import org.springframework.security.core.AuthenticationException;
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.web.filter.OncePerRequestFilter;
|
import org.springframework.web.filter.OncePerRequestFilter;
|
||||||
|
|
||||||
import jakarta.servlet.FilterChain;
|
import jakarta.servlet.FilterChain;
|
||||||
import jakarta.servlet.ServletException;
|
import jakarta.servlet.ServletException;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import stirling.software.SPDF.model.ApiKeyAuthenticationToken;
|
import stirling.software.SPDF.model.ApiKeyAuthenticationToken;
|
||||||
@Component
|
|
||||||
public class UserAuthenticationFilter extends OncePerRequestFilter {
|
@Component
|
||||||
|
public class UserAuthenticationFilter extends OncePerRequestFilter {
|
||||||
@Autowired
|
|
||||||
private UserDetailsService userDetailsService;
|
@Autowired private UserDetailsService userDetailsService;
|
||||||
|
|
||||||
@Autowired
|
@Autowired @Lazy private UserService userService;
|
||||||
@Lazy
|
|
||||||
private UserService userService;
|
@Autowired
|
||||||
|
@Qualifier("loginEnabled")
|
||||||
|
public boolean loginEnabledValue;
|
||||||
@Autowired
|
|
||||||
@Qualifier("loginEnabled")
|
@Override
|
||||||
public boolean loginEnabledValue;
|
protected void doFilterInternal(
|
||||||
|
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||||
@Override
|
throws ServletException, IOException {
|
||||||
protected void doFilterInternal(HttpServletRequest request,
|
|
||||||
HttpServletResponse response,
|
if (!loginEnabledValue) {
|
||||||
FilterChain filterChain) throws ServletException, IOException {
|
// If login is not enabled, just pass all requests without authentication
|
||||||
|
filterChain.doFilter(request, response);
|
||||||
if (!loginEnabledValue) {
|
return;
|
||||||
// If login is not enabled, just pass all requests without authentication
|
}
|
||||||
filterChain.doFilter(request, response);
|
String requestURI = request.getRequestURI();
|
||||||
return;
|
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||||
}
|
|
||||||
String requestURI = request.getRequestURI();
|
// Check for API key in the request headers if no authentication exists
|
||||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
if (authentication == null || !authentication.isAuthenticated()) {
|
||||||
|
String apiKey = request.getHeader("X-API-Key");
|
||||||
// Check for API key in the request headers if no authentication exists
|
if (apiKey != null && !apiKey.trim().isEmpty()) {
|
||||||
if (authentication == null || !authentication.isAuthenticated()) {
|
try {
|
||||||
String apiKey = request.getHeader("X-API-Key");
|
// Use API key to authenticate. This requires you to have an authentication
|
||||||
if (apiKey != null && !apiKey.trim().isEmpty()) {
|
// provider for API keys.
|
||||||
try {
|
UserDetails userDetails = userService.loadUserByApiKey(apiKey);
|
||||||
// Use API key to authenticate. This requires you to have an authentication provider for API keys.
|
if (userDetails == null) {
|
||||||
UserDetails userDetails = userService.loadUserByApiKey(apiKey);
|
response.setStatus(HttpStatus.UNAUTHORIZED.value());
|
||||||
if(userDetails == null)
|
response.getWriter().write("Invalid API Key.");
|
||||||
{
|
return;
|
||||||
response.setStatus(HttpStatus.UNAUTHORIZED.value());
|
}
|
||||||
response.getWriter().write("Invalid API Key.");
|
authentication =
|
||||||
return;
|
new ApiKeyAuthenticationToken(
|
||||||
}
|
userDetails, apiKey, userDetails.getAuthorities());
|
||||||
authentication = new ApiKeyAuthenticationToken(userDetails, apiKey, userDetails.getAuthorities());
|
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
} catch (AuthenticationException e) {
|
||||||
} catch (AuthenticationException e) {
|
// If API key authentication fails, deny the request
|
||||||
// If API key authentication fails, deny the request
|
response.setStatus(HttpStatus.UNAUTHORIZED.value());
|
||||||
response.setStatus(HttpStatus.UNAUTHORIZED.value());
|
response.getWriter().write("Invalid API Key.");
|
||||||
response.getWriter().write("Invalid API Key.");
|
return;
|
||||||
return;
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
// If we still don't have any authentication, deny the request
|
||||||
// If we still don't have any authentication, deny the request
|
if (authentication == null || !authentication.isAuthenticated()) {
|
||||||
if (authentication == null || !authentication.isAuthenticated()) {
|
String method = request.getMethod();
|
||||||
String method = request.getMethod();
|
String contextPath = request.getContextPath();
|
||||||
if ("GET".equalsIgnoreCase(method) && !"/login".equals(requestURI)) {
|
|
||||||
response.sendRedirect("/login"); // redirect to the login page
|
if ("GET".equalsIgnoreCase(method) && !(contextPath + "/login").equals(requestURI)) {
|
||||||
return;
|
response.sendRedirect(contextPath + "/login"); // redirect to the login page
|
||||||
} else {
|
return;
|
||||||
response.setStatus(HttpStatus.UNAUTHORIZED.value());
|
} else {
|
||||||
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");
|
response.setStatus(HttpStatus.UNAUTHORIZED.value());
|
||||||
return;
|
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
|
filterChain.doFilter(request, response);
|
||||||
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
|
}
|
||||||
String uri = request.getRequestURI();
|
|
||||||
|
@Override
|
||||||
String[] permitAllPatterns = {
|
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
|
||||||
"/login",
|
String uri = request.getRequestURI();
|
||||||
"/register",
|
String contextPath = request.getContextPath();
|
||||||
"/error",
|
String[] permitAllPatterns = {
|
||||||
"/images/",
|
contextPath + "/login",
|
||||||
"/public/",
|
contextPath + "/register",
|
||||||
"/css/",
|
contextPath + "/error",
|
||||||
"/js/"
|
contextPath + "/images/",
|
||||||
};
|
contextPath + "/public/",
|
||||||
|
contextPath + "/css/",
|
||||||
for (String pattern : permitAllPatterns) {
|
contextPath + "/js/",
|
||||||
if (uri.startsWith(pattern) || uri.endsWith(".svg")) {
|
contextPath + "/pdfjs/",
|
||||||
return true;
|
contextPath + "/api/v1/info/status",
|
||||||
}
|
contextPath + "/site.webmanifest"
|
||||||
}
|
};
|
||||||
|
|
||||||
return false;
|
for (String pattern : permitAllPatterns) {
|
||||||
}
|
if (uri.startsWith(pattern) || uri.endsWith(".svg")) {
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,125 +1,143 @@
|
|||||||
package stirling.software.SPDF.config.security;
|
package stirling.software.SPDF.config.security;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.core.GrantedAuthority;
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.web.filter.OncePerRequestFilter;
|
import org.springframework.web.filter.OncePerRequestFilter;
|
||||||
|
|
||||||
import io.github.bucket4j.Bandwidth;
|
import io.github.bucket4j.Bandwidth;
|
||||||
import io.github.bucket4j.Bucket;
|
import io.github.bucket4j.Bucket;
|
||||||
import io.github.bucket4j.ConsumptionProbe;
|
import io.github.bucket4j.ConsumptionProbe;
|
||||||
import io.github.bucket4j.Refill;
|
import io.github.bucket4j.Refill;
|
||||||
import jakarta.servlet.FilterChain;
|
|
||||||
import jakarta.servlet.ServletException;
|
import jakarta.servlet.FilterChain;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.ServletException;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import stirling.software.SPDF.model.Role;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
@Component
|
import stirling.software.SPDF.model.Role;
|
||||||
public class UserBasedRateLimitingFilter extends OncePerRequestFilter {
|
|
||||||
|
@Component
|
||||||
private final Map<String, Bucket> apiBuckets = new ConcurrentHashMap<>();
|
public class UserBasedRateLimitingFilter extends OncePerRequestFilter {
|
||||||
private final Map<String, Bucket> webBuckets = new ConcurrentHashMap<>();
|
|
||||||
|
private final Map<String, Bucket> apiBuckets = new ConcurrentHashMap<>();
|
||||||
@Autowired
|
private final Map<String, Bucket> webBuckets = new ConcurrentHashMap<>();
|
||||||
private UserDetailsService userDetailsService;
|
|
||||||
|
@Autowired private UserDetailsService userDetailsService;
|
||||||
@Autowired
|
|
||||||
@Qualifier("rateLimit")
|
@Autowired
|
||||||
public boolean rateLimit;
|
@Qualifier("rateLimit")
|
||||||
|
public boolean rateLimit;
|
||||||
@Override
|
|
||||||
protected void doFilterInternal(HttpServletRequest request,
|
@Override
|
||||||
HttpServletResponse response,
|
protected void doFilterInternal(
|
||||||
FilterChain filterChain) throws ServletException, IOException {
|
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||||
if (!rateLimit) {
|
throws ServletException, IOException {
|
||||||
// If rateLimit is not enabled, just pass all requests without rate limiting
|
if (!rateLimit) {
|
||||||
filterChain.doFilter(request, response);
|
// If rateLimit is not enabled, just pass all requests without rate limiting
|
||||||
return;
|
filterChain.doFilter(request, response);
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
String method = request.getMethod();
|
|
||||||
if (!"POST".equalsIgnoreCase(method)) {
|
String method = request.getMethod();
|
||||||
// If the request is not a POST, just pass it through without rate limiting
|
if (!"POST".equalsIgnoreCase(method)) {
|
||||||
filterChain.doFilter(request, response);
|
// If the request is not a POST, just pass it through without rate limiting
|
||||||
return;
|
filterChain.doFilter(request, response);
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
String identifier = null;
|
|
||||||
|
String identifier = null;
|
||||||
// Check for API key in the request headers
|
|
||||||
String apiKey = request.getHeader("X-API-Key");
|
// Check for API key in the request headers
|
||||||
if (apiKey != null && !apiKey.trim().isEmpty()) {
|
String apiKey = request.getHeader("X-API-Key");
|
||||||
identifier = "API_KEY_" + apiKey; // Prefix to distinguish between API keys and usernames
|
if (apiKey != null && !apiKey.trim().isEmpty()) {
|
||||||
} else {
|
identifier =
|
||||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
"API_KEY_" + apiKey; // Prefix to distinguish between API keys and usernames
|
||||||
if (authentication != null && authentication.isAuthenticated()) {
|
} else {
|
||||||
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
|
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||||
identifier = userDetails.getUsername();
|
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();
|
// 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) {
|
Role userRole =
|
||||||
// It's an API call
|
getRoleFromAuthentication(SecurityContextHolder.getContext().getAuthentication());
|
||||||
processRequest(userRole.getApiCallsPerDay(), identifier, apiBuckets, request, response, filterChain);
|
|
||||||
} else {
|
if (request.getHeader("X-API-Key") != null) {
|
||||||
// It's a Web UI call
|
// It's an API call
|
||||||
processRequest(userRole.getWebCallsPerDay(), identifier, webBuckets, request, response, filterChain);
|
processRequest(
|
||||||
}
|
userRole.getApiCallsPerDay(),
|
||||||
}
|
identifier,
|
||||||
|
apiBuckets,
|
||||||
private Role getRoleFromAuthentication(Authentication authentication) {
|
request,
|
||||||
if (authentication != null && authentication.isAuthenticated()) {
|
response,
|
||||||
for (GrantedAuthority authority : authentication.getAuthorities()) {
|
filterChain);
|
||||||
try {
|
} else {
|
||||||
return Role.fromString(authority.getAuthority());
|
// It's a Web UI call
|
||||||
} catch (IllegalArgumentException ex) {
|
processRequest(
|
||||||
// Ignore and continue to next authority.
|
userRole.getWebCallsPerDay(),
|
||||||
}
|
identifier,
|
||||||
}
|
webBuckets,
|
||||||
}
|
request,
|
||||||
throw new IllegalStateException("User does not have a valid role.");
|
response,
|
||||||
}
|
filterChain);
|
||||||
|
}
|
||||||
private void processRequest(int limitPerDay, String identifier, Map<String, Bucket> buckets,
|
}
|
||||||
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
|
||||||
throws IOException, ServletException {
|
private Role getRoleFromAuthentication(Authentication authentication) {
|
||||||
Bucket userBucket = buckets.computeIfAbsent(identifier, k -> createUserBucket(limitPerDay));
|
if (authentication != null && authentication.isAuthenticated()) {
|
||||||
ConsumptionProbe probe = userBucket.tryConsumeAndReturnRemaining(1);
|
for (GrantedAuthority authority : authentication.getAuthorities()) {
|
||||||
|
try {
|
||||||
if (probe.isConsumed()) {
|
return Role.fromString(authority.getAuthority());
|
||||||
response.setHeader("X-Rate-Limit-Remaining", Long.toString(probe.getRemainingTokens()));
|
} catch (IllegalArgumentException ex) {
|
||||||
filterChain.doFilter(request, response);
|
// Ignore and continue to next authority.
|
||||||
} 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));
|
throw new IllegalStateException("User does not have a valid role.");
|
||||||
response.getWriter().write("Rate limit exceeded for POST requests.");
|
}
|
||||||
}
|
|
||||||
}
|
private void processRequest(
|
||||||
|
int limitPerDay,
|
||||||
private Bucket createUserBucket(int limitPerDay) {
|
String identifier,
|
||||||
Bandwidth limit = Bandwidth.classic(limitPerDay, Refill.intervally(limitPerDay, Duration.ofDays(1)));
|
Map<String, Bucket> buckets,
|
||||||
return Bucket.builder().addLimit(limit).build();
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,191 +1,197 @@
|
|||||||
package stirling.software.SPDF.config.security;
|
package stirling.software.SPDF.config.security;
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.HashMap;
|
import java.util.Collection;
|
||||||
import java.util.Map;
|
import java.util.HashMap;
|
||||||
import java.util.Optional;
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.Optional;
|
||||||
import java.util.stream.Collectors;
|
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.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
import org.springframework.security.core.GrantedAuthority;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
import stirling.software.SPDF.model.Authority;
|
|
||||||
import stirling.software.SPDF.model.User;
|
import stirling.software.SPDF.controller.api.pipeline.UserServiceInterface;
|
||||||
import stirling.software.SPDF.repository.UserRepository;
|
import stirling.software.SPDF.model.Authority;
|
||||||
@Service
|
import stirling.software.SPDF.model.Role;
|
||||||
public class UserService {
|
import stirling.software.SPDF.model.User;
|
||||||
|
import stirling.software.SPDF.repository.UserRepository;
|
||||||
@Autowired
|
|
||||||
private UserRepository userRepository;
|
@Service
|
||||||
|
public class UserService implements UserServiceInterface {
|
||||||
@Autowired
|
|
||||||
private PasswordEncoder passwordEncoder;
|
@Autowired private UserRepository userRepository;
|
||||||
|
|
||||||
public Authentication getAuthentication(String apiKey) {
|
@Autowired private PasswordEncoder passwordEncoder;
|
||||||
User user = getUserByApiKey(apiKey);
|
|
||||||
if (user == null) {
|
public Authentication getAuthentication(String apiKey) {
|
||||||
throw new UsernameNotFoundException("API key is not valid");
|
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)
|
// Convert the user into an Authentication object
|
||||||
null, // credentials (we don't expose the password or API key here)
|
return new UsernamePasswordAuthenticationToken(
|
||||||
getAuthorities(user) // user's authorities (roles/permissions)
|
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()
|
private Collection<? extends GrantedAuthority> getAuthorities(User user) {
|
||||||
.map((Authority authority) -> new SimpleGrantedAuthority(authority.getAuthority()))
|
// Convert each Authority object into a SimpleGrantedAuthority object.
|
||||||
.collect(Collectors.toList());
|
return user.getAuthorities().stream()
|
||||||
|
.map((Authority authority) -> new SimpleGrantedAuthority(authority.getAuthority()))
|
||||||
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
private String generateApiKey() {
|
private String generateApiKey() {
|
||||||
String apiKey;
|
String apiKey;
|
||||||
do {
|
do {
|
||||||
apiKey = UUID.randomUUID().toString();
|
apiKey = UUID.randomUUID().toString();
|
||||||
} while (userRepository.findByApiKey(apiKey) != null); // Ensure uniqueness
|
} while (userRepository.findByApiKey(apiKey) != null); // Ensure uniqueness
|
||||||
return apiKey;
|
return apiKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
public User addApiKeyToUser(String username) {
|
public User addApiKeyToUser(String username) {
|
||||||
User user = userRepository.findByUsername(username)
|
User user =
|
||||||
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
|
userRepository
|
||||||
|
.findByUsername(username)
|
||||||
user.setApiKey(generateApiKey());
|
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
|
||||||
return userRepository.save(user);
|
|
||||||
}
|
user.setApiKey(generateApiKey());
|
||||||
|
return userRepository.save(user);
|
||||||
public User refreshApiKeyForUser(String username) {
|
}
|
||||||
return addApiKeyToUser(username); // reuse the add API key method for refreshing
|
|
||||||
}
|
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"));
|
public String getApiKeyForUser(String username) {
|
||||||
return user.getApiKey();
|
User user =
|
||||||
}
|
userRepository
|
||||||
|
.findByUsername(username)
|
||||||
public boolean isValidApiKey(String apiKey) {
|
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
|
||||||
return userRepository.findByApiKey(apiKey) != null;
|
return user.getApiKey();
|
||||||
}
|
}
|
||||||
|
|
||||||
public User getUserByApiKey(String apiKey) {
|
public boolean isValidApiKey(String apiKey) {
|
||||||
return userRepository.findByApiKey(apiKey);
|
return userRepository.findByApiKey(apiKey) != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public UserDetails loadUserByApiKey(String apiKey) {
|
public User getUserByApiKey(String apiKey) {
|
||||||
User userOptional = userRepository.findByApiKey(apiKey);
|
return userRepository.findByApiKey(apiKey);
|
||||||
if (userOptional != null) {
|
}
|
||||||
User user = userOptional;
|
|
||||||
// Convert your User entity to a UserDetails object with authorities
|
public UserDetails loadUserByApiKey(String apiKey) {
|
||||||
return new org.springframework.security.core.userdetails.User(
|
User userOptional = userRepository.findByApiKey(apiKey);
|
||||||
user.getUsername(),
|
if (userOptional != null) {
|
||||||
user.getPassword(), // you might not need this for API key auth
|
User user = userOptional;
|
||||||
getAuthorities(user)
|
// Convert your User entity to a UserDetails object with authorities
|
||||||
);
|
return new org.springframework.security.core.userdetails.User(
|
||||||
}
|
user.getUsername(),
|
||||||
return null; // or throw an exception
|
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 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);
|
public void saveUser(String username, String password) {
|
||||||
user.setPassword(passwordEncoder.encode(password));
|
User user = new User();
|
||||||
user.setEnabled(true);
|
user.setUsername(username);
|
||||||
userRepository.save(user);
|
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);
|
public void saveUser(String username, String password, String role, boolean firstLogin) {
|
||||||
user.setPassword(passwordEncoder.encode(password));
|
User user = new User();
|
||||||
user.addAuthority(new Authority(role, user));
|
user.setUsername(username);
|
||||||
user.setEnabled(true);
|
user.setPassword(passwordEncoder.encode(password));
|
||||||
user.setFirstLogin(firstLogin);
|
user.addAuthority(new Authority(role, user));
|
||||||
userRepository.save(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);
|
public void saveUser(String username, String password, String role) {
|
||||||
user.setPassword(passwordEncoder.encode(password));
|
User user = new User();
|
||||||
user.addAuthority(new Authority(role, user));
|
user.setUsername(username);
|
||||||
user.setEnabled(true);
|
user.setPassword(passwordEncoder.encode(password));
|
||||||
user.setFirstLogin(false);
|
user.addAuthority(new Authority(role, user));
|
||||||
userRepository.save(user);
|
user.setEnabled(true);
|
||||||
}
|
user.setFirstLogin(false);
|
||||||
|
userRepository.save(user);
|
||||||
public void deleteUser(String username) {
|
}
|
||||||
Optional<User> userOpt = userRepository.findByUsername(username);
|
|
||||||
if (userOpt.isPresent()) {
|
public void deleteUser(String username) {
|
||||||
userRepository.delete(userOpt.get());
|
Optional<User> userOpt = userRepository.findByUsername(username);
|
||||||
}
|
if (userOpt.isPresent()) {
|
||||||
}
|
for (Authority authority : userOpt.get().getAuthorities()) {
|
||||||
|
if (authority.getAuthority().equals(Role.INTERNAL_API_USER.getRoleId())) {
|
||||||
public boolean usernameExists(String username) {
|
return;
|
||||||
return userRepository.findByUsername(username).isPresent();
|
}
|
||||||
}
|
}
|
||||||
|
userRepository.delete(userOpt.get());
|
||||||
public boolean hasUsers() {
|
}
|
||||||
return userRepository.count() > 0;
|
}
|
||||||
}
|
|
||||||
|
public boolean usernameExists(String username) {
|
||||||
public void updateUserSettings(String username, Map<String, String> updates) {
|
return userRepository.findByUsername(username).isPresent();
|
||||||
Optional<User> userOpt = userRepository.findByUsername(username);
|
}
|
||||||
if (userOpt.isPresent()) {
|
|
||||||
User user = userOpt.get();
|
public boolean hasUsers() {
|
||||||
Map<String, String> settingsMap = user.getSettings();
|
return userRepository.count() > 0;
|
||||||
|
}
|
||||||
if(settingsMap == null) {
|
|
||||||
settingsMap = new HashMap<String,String>();
|
public void updateUserSettings(String username, Map<String, String> updates) {
|
||||||
}
|
Optional<User> userOpt = userRepository.findByUsername(username);
|
||||||
settingsMap.clear();
|
if (userOpt.isPresent()) {
|
||||||
settingsMap.putAll(updates);
|
User user = userOpt.get();
|
||||||
user.setSettings(settingsMap);
|
Map<String, String> settingsMap = user.getSettings();
|
||||||
|
|
||||||
userRepository.save(user);
|
if (settingsMap == null) {
|
||||||
}
|
settingsMap = new HashMap<String, String>();
|
||||||
}
|
}
|
||||||
|
settingsMap.clear();
|
||||||
public Optional<User> findByUsername(String username) {
|
settingsMap.putAll(updates);
|
||||||
return userRepository.findByUsername(username);
|
user.setSettings(settingsMap);
|
||||||
}
|
|
||||||
|
userRepository.save(user);
|
||||||
public void changeUsername(User user, String newUsername) {
|
}
|
||||||
user.setUsername(newUsername);
|
}
|
||||||
userRepository.save(user);
|
|
||||||
}
|
public Optional<User> findByUsername(String username) {
|
||||||
|
return userRepository.findByUsername(username);
|
||||||
public void changePassword(User user, String newPassword) {
|
}
|
||||||
user.setPassword(passwordEncoder.encode(newPassword));
|
|
||||||
userRepository.save(user);
|
public void changeUsername(User user, String newUsername) {
|
||||||
}
|
user.setUsername(newUsername);
|
||||||
|
userRepository.save(user);
|
||||||
public void changeFirstUse(User user, boolean firstUse) {
|
}
|
||||||
user.setFirstLogin(firstUse);
|
|
||||||
userRepository.save(user);
|
public void changePassword(User user, String newPassword) {
|
||||||
}
|
user.setPassword(passwordEncoder.encode(newPassword));
|
||||||
|
userRepository.save(user);
|
||||||
|
}
|
||||||
public boolean isPasswordCorrect(User user, String currentPassword) {
|
|
||||||
return passwordEncoder.matches(currentPassword, user.getPassword());
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
package stirling.software.SPDF.controller.api;
|
package stirling.software.SPDF.controller.api;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import org.apache.pdfbox.Loader;
|
||||||
import org.apache.pdfbox.multipdf.LayerUtility;
|
import org.apache.pdfbox.multipdf.LayerUtility;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
import org.apache.pdfbox.pdmodel.PDPage;
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDPageContentStream.AppendMode;
|
||||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||||
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
|
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@@ -20,6 +21,7 @@ import org.springframework.web.bind.annotation.RestController;
|
|||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.general.CropPdfForm;
|
import stirling.software.SPDF.model.api.general.CropPdfForm;
|
||||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
|
||||||
@@ -28,59 +30,61 @@ import stirling.software.SPDF.utils.WebResponseUtils;
|
|||||||
@Tag(name = "General", description = "General APIs")
|
@Tag(name = "General", description = "General APIs")
|
||||||
public class CropController {
|
public class CropController {
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(CropController.class);
|
private static final Logger logger = LoggerFactory.getLogger(CropController.class);
|
||||||
|
|
||||||
@PostMapping(value = "/crop", consumes = "multipart/form-data")
|
@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")
|
@Operation(
|
||||||
public ResponseEntity<byte[]> cropPdf(@ModelAttribute CropPdfForm form)
|
summary = "Crops a PDF document",
|
||||||
throws IOException {
|
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(@ModelAttribute CropPdfForm form) throws IOException {
|
||||||
|
PDDocument sourceDocument = Loader.loadPDF(form.getFileInput().getBytes());
|
||||||
|
|
||||||
|
PDDocument newDocument = new PDDocument();
|
||||||
|
|
||||||
|
int totalPages = sourceDocument.getNumberOfPages();
|
||||||
|
|
||||||
PDDocument sourceDocument = PDDocument.load(new ByteArrayInputStream(form.getFileInput().getBytes()));
|
LayerUtility layerUtility = new LayerUtility(newDocument);
|
||||||
|
|
||||||
PDDocument newDocument = new PDDocument();
|
for (int i = 0; i < totalPages; i++) {
|
||||||
|
PDPage sourcePage = sourceDocument.getPage(i);
|
||||||
|
|
||||||
int totalPages = sourceDocument.getNumberOfPages();
|
// 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, AppendMode.OVERWRITE, true, true);
|
||||||
|
|
||||||
LayerUtility layerUtility = new LayerUtility(newDocument);
|
// Import the source page as a form XObject
|
||||||
|
PDFormXObject formXObject = layerUtility.importPageAsForm(sourceDocument, i);
|
||||||
|
|
||||||
for (int i = 0; i < totalPages; i++) {
|
contentStream.saveGraphicsState();
|
||||||
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
|
// Define the crop area
|
||||||
PDFormXObject formXObject = layerUtility.importPageAsForm(sourceDocument, i);
|
contentStream.addRect(form.getX(), form.getY(), form.getWidth(), form.getHeight());
|
||||||
|
contentStream.clip();
|
||||||
|
|
||||||
contentStream.saveGraphicsState();
|
// Draw the entire formXObject
|
||||||
|
contentStream.drawForm(formXObject);
|
||||||
// Define the crop area
|
|
||||||
contentStream.addRect(form.getX(), form.getY(), form.getWidth(), form.getHeight());
|
|
||||||
contentStream.clip();
|
|
||||||
|
|
||||||
// Draw the entire formXObject
|
contentStream.restoreGraphicsState();
|
||||||
contentStream.drawForm(formXObject);
|
|
||||||
|
|
||||||
contentStream.restoreGraphicsState();
|
contentStream.close();
|
||||||
|
|
||||||
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");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// 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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
package stirling.software.SPDF.controller.api;
|
package stirling.software.SPDF.controller.api;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.nio.file.attribute.BasicFileAttributes;
|
import java.nio.file.attribute.BasicFileAttributes;
|
||||||
@@ -10,6 +11,8 @@ import java.util.Arrays;
|
|||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.apache.pdfbox.Loader;
|
||||||
|
import org.apache.pdfbox.multipdf.PDFMergerUtility;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
import org.apache.pdfbox.pdmodel.PDPage;
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@@ -23,7 +26,9 @@ import org.springframework.web.multipart.MultipartFile;
|
|||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.general.MergePdfsRequest;
|
import stirling.software.SPDF.model.api.general.MergePdfsRequest;
|
||||||
|
import stirling.software.SPDF.utils.GeneralUtils;
|
||||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@@ -33,83 +38,105 @@ public class MergeController {
|
|||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(MergeController.class);
|
private static final Logger logger = LoggerFactory.getLogger(MergeController.class);
|
||||||
|
|
||||||
|
private PDDocument mergeDocuments(List<PDDocument> documents) throws IOException {
|
||||||
private PDDocument mergeDocuments(List<PDDocument> documents) throws IOException {
|
PDDocument mergedDoc = new PDDocument();
|
||||||
PDDocument mergedDoc = new PDDocument();
|
|
||||||
for (PDDocument doc : documents) {
|
|
||||||
for (PDPage page : doc.getPages()) {
|
|
||||||
mergedDoc.addPage(page);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return mergedDoc;
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@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 {
|
|
||||||
|
|
||||||
MultipartFile[] files = form.getFileInput();
|
|
||||||
Arrays.sort(files, getSortComparator(form.getSortType()));
|
|
||||||
|
|
||||||
List<PDDocument> documents = new ArrayList<>();
|
|
||||||
for (MultipartFile file : files) {
|
|
||||||
try (InputStream is = file.getInputStream()) {
|
|
||||||
documents.add(PDDocument.load(is));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try (PDDocument mergedDoc = mergeDocuments(documents)) {
|
|
||||||
ResponseEntity<byte[]> response = WebResponseUtils.pdfDocToWebResponse(mergedDoc, files[0].getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_merged.pdf");
|
|
||||||
return response;
|
|
||||||
} finally {
|
|
||||||
for (PDDocument doc : documents) {
|
for (PDDocument doc : documents) {
|
||||||
if (doc != null) {
|
for (PDPage page : doc.getPages()) {
|
||||||
doc.close();
|
mergedDoc.addPage(page);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mergedDoc;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = Loader.loadPDF(file1.getBytes());
|
||||||
|
PDDocument doc2 = Loader.loadPDF(file2.getBytes())) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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 {
|
||||||
|
List<File> filesToDelete = new ArrayList<File>();
|
||||||
|
try {
|
||||||
|
MultipartFile[] files = form.getFileInput();
|
||||||
|
Arrays.sort(files, getSortComparator(form.getSortType()));
|
||||||
|
|
||||||
|
PDFMergerUtility mergedDoc = new PDFMergerUtility();
|
||||||
|
ByteArrayOutputStream docOutputstream = new ByteArrayOutputStream();
|
||||||
|
|
||||||
|
for (MultipartFile multipartFile : files) {
|
||||||
|
File tempFile = GeneralUtils.convertMultipartFileToFile(multipartFile);
|
||||||
|
filesToDelete.add(tempFile);
|
||||||
|
mergedDoc.addSource(tempFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
mergedDoc.setDestinationFileName(
|
||||||
|
files[0].getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_merged.pdf");
|
||||||
|
mergedDoc.setDestinationStream(docOutputstream);
|
||||||
|
|
||||||
|
mergedDoc.mergeDocuments(null);
|
||||||
|
|
||||||
|
return WebResponseUtils.bytesToWebResponse(
|
||||||
|
docOutputstream.toByteArray(), mergedDoc.getDestinationFileName());
|
||||||
|
} catch (Exception ex) {
|
||||||
|
logger.error("Error in merge pdf process", ex);
|
||||||
|
throw ex;
|
||||||
|
} finally {
|
||||||
|
for (File file : filesToDelete) {
|
||||||
|
file.delete();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
package stirling.software.SPDF.controller.api;
|
package stirling.software.SPDF.controller.api;
|
||||||
|
|
||||||
|
import java.awt.Color;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import org.apache.pdfbox.Loader;
|
||||||
import org.apache.pdfbox.multipdf.LayerUtility;
|
import org.apache.pdfbox.multipdf.LayerUtility;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
import org.apache.pdfbox.pdmodel.PDPage;
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
@@ -20,8 +21,10 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
|||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import io.github.pixee.security.Filenames;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.general.MergeMultiplePagesRequest;
|
import stirling.software.SPDF.model.api.general.MergeMultiplePagesRequest;
|
||||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
|
||||||
@@ -30,81 +33,111 @@ import stirling.software.SPDF.utils.WebResponseUtils;
|
|||||||
@Tag(name = "General", description = "General APIs")
|
@Tag(name = "General", description = "General APIs")
|
||||||
public class MultiPageLayoutController {
|
public class MultiPageLayoutController {
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(MultiPageLayoutController.class);
|
private static final Logger logger = LoggerFactory.getLogger(MultiPageLayoutController.class);
|
||||||
|
|
||||||
@PostMapping(value = "/multi-page-layout", consumes = "multipart/form-data")
|
@PostMapping(value = "/multi-page-layout", consumes = "multipart/form-data")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Merge multiple pages of a PDF document into a single page",
|
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"
|
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)
|
public ResponseEntity<byte[]> mergeMultiplePagesIntoOne(
|
||||||
throws IOException {
|
@ModelAttribute MergeMultiplePagesRequest request) throws IOException {
|
||||||
|
|
||||||
int pagesPerSheet = request.getPagesPerSheet();
|
int pagesPerSheet = request.getPagesPerSheet();
|
||||||
MultipartFile file = request.getFileInput();
|
MultipartFile file = request.getFileInput();
|
||||||
|
boolean addBorder = request.isAddBorder();
|
||||||
|
|
||||||
if (pagesPerSheet != 2 && pagesPerSheet != 3 && pagesPerSheet != (int) Math.sqrt(pagesPerSheet) * Math.sqrt(pagesPerSheet)) {
|
if (pagesPerSheet != 2
|
||||||
throw new IllegalArgumentException("pagesPerSheet must be 2, 3 or a perfect square");
|
&& 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 cols =
|
||||||
int rows = pagesPerSheet == 2 || pagesPerSheet == 3 ? 1 : (int) Math.sqrt(pagesPerSheet);
|
pagesPerSheet == 2 || pagesPerSheet == 3
|
||||||
|
? pagesPerSheet
|
||||||
|
: (int) Math.sqrt(pagesPerSheet);
|
||||||
|
int rows = pagesPerSheet == 2 || pagesPerSheet == 3 ? 1 : (int) Math.sqrt(pagesPerSheet);
|
||||||
|
|
||||||
PDDocument sourceDocument = PDDocument.load(file.getInputStream());
|
PDDocument sourceDocument = Loader.loadPDF(file.getBytes());
|
||||||
PDDocument newDocument = new PDDocument();
|
PDDocument newDocument = new PDDocument();
|
||||||
PDPage newPage = new PDPage(PDRectangle.A4);
|
PDPage newPage = new PDPage(PDRectangle.A4);
|
||||||
newDocument.addPage(newPage);
|
newDocument.addPage(newPage);
|
||||||
|
|
||||||
int totalPages = sourceDocument.getNumberOfPages();
|
int totalPages = sourceDocument.getNumberOfPages();
|
||||||
float cellWidth = newPage.getMediaBox().getWidth() / cols;
|
float cellWidth = newPage.getMediaBox().getWidth() / cols;
|
||||||
float cellHeight = newPage.getMediaBox().getHeight() / rows;
|
float cellHeight = newPage.getMediaBox().getHeight() / rows;
|
||||||
|
|
||||||
PDPageContentStream contentStream = new PDPageContentStream(newDocument, newPage, PDPageContentStream.AppendMode.APPEND, true, true);
|
PDPageContentStream contentStream =
|
||||||
LayerUtility layerUtility = new LayerUtility(newDocument);
|
new PDPageContentStream(
|
||||||
|
newDocument, newPage, PDPageContentStream.AppendMode.APPEND, true, true);
|
||||||
|
LayerUtility layerUtility = new LayerUtility(newDocument);
|
||||||
|
|
||||||
for (int i = 0; i < totalPages; i++) {
|
float borderThickness = 1.5f; // Specify border thickness as required
|
||||||
if (i != 0 && i % pagesPerSheet == 0) {
|
contentStream.setLineWidth(borderThickness);
|
||||||
// Close the current content stream and create a new page and content stream
|
contentStream.setStrokingColor(Color.BLACK);
|
||||||
contentStream.close();
|
|
||||||
newPage = new PDPage(PDRectangle.A4);
|
|
||||||
newDocument.addPage(newPage);
|
|
||||||
contentStream = new PDPageContentStream(newDocument, newPage, PDPageContentStream.AppendMode.APPEND, true, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
PDPage sourcePage = sourceDocument.getPage(i);
|
for (int i = 0; i < totalPages; i++) {
|
||||||
PDRectangle rect = sourcePage.getMediaBox();
|
if (i != 0 && i % pagesPerSheet == 0) {
|
||||||
float scaleWidth = cellWidth / rect.getWidth();
|
// Close the current content stream and create a new page and content stream
|
||||||
float scaleHeight = cellHeight / rect.getHeight();
|
contentStream.close();
|
||||||
float scale = Math.min(scaleWidth, scaleHeight);
|
newPage = new PDPage(PDRectangle.A4);
|
||||||
|
newDocument.addPage(newPage);
|
||||||
|
contentStream =
|
||||||
|
new PDPageContentStream(
|
||||||
|
newDocument,
|
||||||
|
newPage,
|
||||||
|
PDPageContentStream.AppendMode.APPEND,
|
||||||
|
true,
|
||||||
|
true);
|
||||||
|
}
|
||||||
|
|
||||||
int adjustedPageIndex = i % pagesPerSheet; // This will reset the index for every new page
|
PDPage sourcePage = sourceDocument.getPage(i);
|
||||||
int rowIndex = adjustedPageIndex / cols;
|
PDRectangle rect = sourcePage.getMediaBox();
|
||||||
int colIndex = adjustedPageIndex % cols;
|
float scaleWidth = cellWidth / rect.getWidth();
|
||||||
|
float scaleHeight = cellHeight / rect.getHeight();
|
||||||
|
float scale = Math.min(scaleWidth, scaleHeight);
|
||||||
|
|
||||||
float x = colIndex * cellWidth + (cellWidth - rect.getWidth() * scale) / 2;
|
int adjustedPageIndex =
|
||||||
float y = newPage.getMediaBox().getHeight() - ((rowIndex + 1) * cellHeight - (cellHeight - rect.getHeight() * scale) / 2);
|
i % pagesPerSheet; // This will reset the index for every new page
|
||||||
|
int rowIndex = adjustedPageIndex / cols;
|
||||||
|
int colIndex = adjustedPageIndex % cols;
|
||||||
|
|
||||||
contentStream.saveGraphicsState();
|
float x = colIndex * cellWidth + (cellWidth - rect.getWidth() * scale) / 2;
|
||||||
contentStream.transform(Matrix.getTranslateInstance(x, y));
|
float y =
|
||||||
contentStream.transform(Matrix.getScaleInstance(scale, scale));
|
newPage.getMediaBox().getHeight()
|
||||||
|
- ((rowIndex + 1) * cellHeight
|
||||||
|
- (cellHeight - rect.getHeight() * scale) / 2);
|
||||||
|
|
||||||
PDFormXObject formXObject = layerUtility.importPageAsForm(sourceDocument, i);
|
contentStream.saveGraphicsState();
|
||||||
contentStream.drawForm(formXObject);
|
contentStream.transform(Matrix.getTranslateInstance(x, y));
|
||||||
|
contentStream.transform(Matrix.getScaleInstance(scale, scale));
|
||||||
|
|
||||||
contentStream.restoreGraphicsState();
|
PDFormXObject formXObject = layerUtility.importPageAsForm(sourceDocument, i);
|
||||||
}
|
contentStream.drawForm(formXObject);
|
||||||
|
|
||||||
|
contentStream.restoreGraphicsState();
|
||||||
|
|
||||||
contentStream.close(); // Close the final content stream
|
if (addBorder) {
|
||||||
sourceDocument.close();
|
// 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
contentStream.close(); // Close the final content stream
|
||||||
newDocument.save(baos);
|
sourceDocument.close();
|
||||||
newDocument.close();
|
|
||||||
|
|
||||||
byte[] result = baos.toByteArray();
|
|
||||||
return WebResponseUtils.bytesToWebResponse(result, file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_layoutChanged.pdf");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
newDocument.save(baos);
|
||||||
|
newDocument.close();
|
||||||
|
|
||||||
|
byte[] result = baos.toByteArray();
|
||||||
|
return WebResponseUtils.bytesToWebResponse(
|
||||||
|
result,
|
||||||
|
Filenames.toSimpleFileName(file.getOriginalFilename()).replaceFirst("[.][^.]+$", "")
|
||||||
|
+ "_layoutChanged.pdf");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,202 @@
|
|||||||
|
package stirling.software.SPDF.controller.api;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.apache.pdfbox.Loader;
|
||||||
|
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.github.pixee.security.Filenames;
|
||||||
|
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 = Loader.loadPDF(baseFile.getBytes());
|
||||||
|
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 =
|
||||||
|
Filenames.toSimpleFileName(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 = Loader.loadPDF(overlayFiles[overlayFileIndex])) {
|
||||||
|
PDDocument singlePageDocument = new PDDocument();
|
||||||
|
singlePageDocument.addPage(overlayPdf.getPage(pageCountInCurrentOverlay));
|
||||||
|
File tempFile = Files.createTempFile("overlay-page-", ".pdf").toFile();
|
||||||
|
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 = Loader.loadPDF(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 = Loader.loadPDF(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 = Loader.loadPDF(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.
|
||||||
@@ -4,6 +4,7 @@ import java.io.IOException;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.apache.pdfbox.Loader;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
import org.apache.pdfbox.pdmodel.PDPage;
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@@ -12,206 +13,213 @@ import org.springframework.http.ResponseEntity;
|
|||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
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.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import io.github.pixee.security.Filenames;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.Parameter;
|
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.SortTypes;
|
import stirling.software.SPDF.model.SortTypes;
|
||||||
|
import stirling.software.SPDF.model.api.PDFWithPageNums;
|
||||||
import stirling.software.SPDF.model.api.general.RearrangePagesRequest;
|
import stirling.software.SPDF.model.api.general.RearrangePagesRequest;
|
||||||
import stirling.software.SPDF.utils.GeneralUtils;
|
import stirling.software.SPDF.utils.GeneralUtils;
|
||||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/general")
|
@RequestMapping("/api/v1/general")
|
||||||
@Tag(name = "General", description = "General APIs")
|
@Tag(name = "General", description = "General APIs")
|
||||||
public class RearrangePagesPDFController {
|
public class RearrangePagesPDFController {
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(RearrangePagesPDFController.class);
|
private static final Logger logger = LoggerFactory.getLogger(RearrangePagesPDFController.class);
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/remove-pages")
|
@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")
|
@Operation(
|
||||||
public ResponseEntity<byte[]> deletePages(
|
summary = "Remove pages from a PDF file",
|
||||||
@RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file from which pages will be removed") MultipartFile pdfFile,
|
description =
|
||||||
@RequestParam("pagesToDelete") @Parameter(description = "Comma-separated list of pages or page ranges to delete, e.g., '1,3,5-8'") String pagesToDelete)
|
"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")
|
||||||
throws IOException {
|
public ResponseEntity<byte[]> deletePages(@ModelAttribute PDFWithPageNums request)
|
||||||
|
throws IOException {
|
||||||
|
|
||||||
PDDocument document = PDDocument.load(pdfFile.getBytes());
|
MultipartFile pdfFile = request.getFileInput();
|
||||||
|
String pagesToDelete = request.getPageNumbers();
|
||||||
|
|
||||||
// Split the page order string into an array of page numbers or range of numbers
|
PDDocument document = Loader.loadPDF(pdfFile.getBytes());
|
||||||
String[] pageOrderArr = pagesToDelete.split(",");
|
|
||||||
|
|
||||||
List<Integer> pagesToRemove = GeneralUtils.parsePageList(pageOrderArr, document.getNumberOfPages());
|
// Split the page order string into an array of page numbers or range of numbers
|
||||||
|
String[] pageOrderArr = pagesToDelete.split(",");
|
||||||
|
|
||||||
for (int i = pagesToRemove.size() - 1; i >= 0; i--) {
|
List<Integer> pagesToRemove =
|
||||||
int pageIndex = pagesToRemove.get(i);
|
GeneralUtils.parsePageList(pageOrderArr, document.getNumberOfPages());
|
||||||
document.removePage(pageIndex);
|
|
||||||
}
|
|
||||||
return WebResponseUtils.pdfDocToWebResponse(document,
|
|
||||||
pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_removed_pages.pdf");
|
|
||||||
|
|
||||||
}
|
for (int i = pagesToRemove.size() - 1; i >= 0; i--) {
|
||||||
|
int pageIndex = pagesToRemove.get(i);
|
||||||
|
document.removePage(pageIndex);
|
||||||
|
}
|
||||||
private List<Integer> removeFirst(int totalPages) {
|
return WebResponseUtils.pdfDocToWebResponse(
|
||||||
if (totalPages <= 1)
|
document,
|
||||||
return new ArrayList<>();
|
Filenames.toSimpleFileName(pdfFile.getOriginalFilename())
|
||||||
List<Integer> newPageOrder = new ArrayList<>();
|
.replaceFirst("[.][^.]+$", "")
|
||||||
for (int i = 2; i <= totalPages; i++) {
|
+ "_removed_pages.pdf");
|
||||||
newPageOrder.add(i - 1);
|
|
||||||
}
|
|
||||||
return newPageOrder;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<Integer> removeLast(int totalPages) {
|
|
||||||
if (totalPages <= 1)
|
|
||||||
return new ArrayList<>();
|
|
||||||
List<Integer> newPageOrder = new ArrayList<>();
|
|
||||||
for (int i = 1; i < totalPages; i++) {
|
|
||||||
newPageOrder.add(i - 1);
|
|
||||||
}
|
|
||||||
return newPageOrder;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<Integer> removeFirstAndLast(int totalPages) {
|
|
||||||
if (totalPages <= 2)
|
|
||||||
return new ArrayList<>();
|
|
||||||
List<Integer> newPageOrder = new ArrayList<>();
|
|
||||||
for (int i = 2; i < totalPages; i++) {
|
|
||||||
newPageOrder.add(i - 1);
|
|
||||||
}
|
|
||||||
return newPageOrder;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<Integer> reverseOrder(int totalPages) {
|
|
||||||
List<Integer> newPageOrder = new ArrayList<>();
|
|
||||||
for (int i = totalPages; i >= 1; i--) {
|
|
||||||
newPageOrder.add(i - 1);
|
|
||||||
}
|
|
||||||
return newPageOrder;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<Integer> duplexSort(int totalPages) {
|
|
||||||
List<Integer> newPageOrder = new ArrayList<>();
|
|
||||||
int half = (totalPages + 1) / 2; // This ensures proper behavior with odd numbers of pages
|
|
||||||
for (int i = 1; i <= half; i++) {
|
|
||||||
newPageOrder.add(i - 1);
|
|
||||||
if (i <= totalPages - half) { // Avoid going out of bounds
|
|
||||||
newPageOrder.add(totalPages - i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return newPageOrder;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<Integer> bookletSort(int totalPages) {
|
|
||||||
List<Integer> newPageOrder = new ArrayList<>();
|
|
||||||
for (int i = 0; i < totalPages / 2; i++) {
|
|
||||||
newPageOrder.add(i);
|
|
||||||
newPageOrder.add(totalPages - i - 1);
|
|
||||||
}
|
|
||||||
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) {
|
private List<Integer> removeFirst(int totalPages) {
|
||||||
List<Integer> newPageOrder = new ArrayList<>();
|
if (totalPages <= 1) return new ArrayList<>();
|
||||||
for (int i = 1; i <= totalPages; i += 2) {
|
List<Integer> newPageOrder = new ArrayList<>();
|
||||||
newPageOrder.add(i - 1);
|
for (int i = 2; i <= totalPages; i++) {
|
||||||
}
|
newPageOrder.add(i - 1);
|
||||||
for (int i = 2; i <= totalPages; i += 2) {
|
}
|
||||||
newPageOrder.add(i - 1);
|
return newPageOrder;
|
||||||
}
|
}
|
||||||
return newPageOrder;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<Integer> processSortTypes(String sortTypes, int totalPages) {
|
private List<Integer> removeLast(int totalPages) {
|
||||||
try {
|
if (totalPages <= 1) return new ArrayList<>();
|
||||||
SortTypes mode = SortTypes.valueOf(sortTypes.toUpperCase());
|
List<Integer> newPageOrder = new ArrayList<>();
|
||||||
switch (mode) {
|
for (int i = 1; i < totalPages; i++) {
|
||||||
case REVERSE_ORDER:
|
newPageOrder.add(i - 1);
|
||||||
return reverseOrder(totalPages);
|
}
|
||||||
case DUPLEX_SORT:
|
return newPageOrder;
|
||||||
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:
|
|
||||||
return removeFirst(totalPages);
|
|
||||||
case REMOVE_LAST:
|
|
||||||
return removeLast(totalPages);
|
|
||||||
case REMOVE_FIRST_AND_LAST:
|
|
||||||
return removeFirstAndLast(totalPages);
|
|
||||||
default:
|
|
||||||
throw new IllegalArgumentException("Unsupported custom mode");
|
|
||||||
}
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
logger.error("Unsupported custom mode", e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/rearrange-pages")
|
private List<Integer> removeFirstAndLast(int totalPages) {
|
||||||
@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")
|
if (totalPages <= 2) return new ArrayList<>();
|
||||||
public ResponseEntity<byte[]> rearrangePages(@ModelAttribute RearrangePagesRequest request) throws IOException {
|
List<Integer> newPageOrder = new ArrayList<>();
|
||||||
MultipartFile pdfFile = request.getFileInput();
|
for (int i = 2; i < totalPages; i++) {
|
||||||
String pageOrder = request.getPageNumbers();
|
newPageOrder.add(i - 1);
|
||||||
String sortType = request.getCustomMode();
|
}
|
||||||
try {
|
return newPageOrder;
|
||||||
// Load the input PDF
|
}
|
||||||
PDDocument document = PDDocument.load(pdfFile.getInputStream());
|
|
||||||
|
|
||||||
// Split the page order string into an array of page numbers or range of numbers
|
private List<Integer> reverseOrder(int totalPages) {
|
||||||
String[] pageOrderArr = pageOrder != null ? pageOrder.split(",") : new String[0];
|
List<Integer> newPageOrder = new ArrayList<>();
|
||||||
int totalPages = document.getNumberOfPages();
|
for (int i = totalPages; i >= 1; i--) {
|
||||||
List<Integer> newPageOrder;
|
newPageOrder.add(i - 1);
|
||||||
if (sortType != null && sortType.length() > 0) {
|
}
|
||||||
newPageOrder = processSortTypes(sortType, totalPages);
|
return newPageOrder;
|
||||||
} 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++) {
|
|
||||||
newPages.add(document.getPage(newPageOrder.get(i)));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove all the pages from the original document
|
private List<Integer> duplexSort(int totalPages) {
|
||||||
for (int i = document.getNumberOfPages() - 1; i >= 0; i--) {
|
List<Integer> newPageOrder = new ArrayList<>();
|
||||||
document.removePage(i);
|
int half = (totalPages + 1) / 2; // This ensures proper behavior with odd numbers of pages
|
||||||
}
|
for (int i = 1; i <= half; i++) {
|
||||||
|
newPageOrder.add(i - 1);
|
||||||
|
if (i <= totalPages - half) { // Avoid going out of bounds
|
||||||
|
newPageOrder.add(totalPages - i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newPageOrder;
|
||||||
|
}
|
||||||
|
|
||||||
// Add the pages in the new order
|
private List<Integer> bookletSort(int totalPages) {
|
||||||
for (PDPage page : newPages) {
|
List<Integer> newPageOrder = new ArrayList<>();
|
||||||
document.addPage(page);
|
for (int i = 0; i < totalPages / 2; i++) {
|
||||||
}
|
newPageOrder.add(i);
|
||||||
|
newPageOrder.add(totalPages - i - 1);
|
||||||
|
}
|
||||||
|
return newPageOrder;
|
||||||
|
}
|
||||||
|
|
||||||
return WebResponseUtils.pdfDocToWebResponse(document,
|
private List<Integer> sideStitchBooklet(int totalPages) {
|
||||||
pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_rearranged.pdf");
|
List<Integer> newPageOrder = new ArrayList<>();
|
||||||
} catch (IOException e) {
|
for (int i = 0; i < (totalPages + 3) / 4; i++) {
|
||||||
logger.error("Failed rearranging documents", e);
|
int begin = i * 4;
|
||||||
return null;
|
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) {
|
||||||
|
newPageOrder.add(i - 1);
|
||||||
|
}
|
||||||
|
for (int i = 2; i <= totalPages; i += 2) {
|
||||||
|
newPageOrder.add(i - 1);
|
||||||
|
}
|
||||||
|
return newPageOrder;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Integer> processSortTypes(String sortTypes, int totalPages) {
|
||||||
|
try {
|
||||||
|
SortTypes mode = SortTypes.valueOf(sortTypes.toUpperCase());
|
||||||
|
switch (mode) {
|
||||||
|
case REVERSE_ORDER:
|
||||||
|
return reverseOrder(totalPages);
|
||||||
|
case DUPLEX_SORT:
|
||||||
|
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:
|
||||||
|
return removeFirst(totalPages);
|
||||||
|
case REMOVE_LAST:
|
||||||
|
return removeLast(totalPages);
|
||||||
|
case REMOVE_FIRST_AND_LAST:
|
||||||
|
return removeFirstAndLast(totalPages);
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException("Unsupported custom mode");
|
||||||
|
}
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
logger.error("Unsupported custom mode", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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(@ModelAttribute RearrangePagesRequest request)
|
||||||
|
throws IOException {
|
||||||
|
MultipartFile pdfFile = request.getFileInput();
|
||||||
|
String pageOrder = request.getPageNumbers();
|
||||||
|
String sortType = request.getCustomMode();
|
||||||
|
try {
|
||||||
|
// Load the input PDF
|
||||||
|
PDDocument document = Loader.loadPDF(pdfFile.getBytes());
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
List<Integer> newPageOrder;
|
||||||
|
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++) {
|
||||||
|
newPages.add(document.getPage(newPageOrder.get(i)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove all the pages from the original document
|
||||||
|
for (int i = document.getNumberOfPages() - 1; i >= 0; i--) {
|
||||||
|
document.removePage(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the pages in the new order
|
||||||
|
for (PDPage page : newPages) {
|
||||||
|
document.addPage(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
return WebResponseUtils.pdfDocToWebResponse(
|
||||||
|
document,
|
||||||
|
Filenames.toSimpleFileName(pdfFile.getOriginalFilename())
|
||||||
|
.replaceFirst("[.][^.]+$", "")
|
||||||
|
+ "_rearranged.pdf");
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.error("Failed rearranging documents", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package stirling.software.SPDF.controller.api;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import org.apache.pdfbox.Loader;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
import org.apache.pdfbox.pdmodel.PDPage;
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
import org.apache.pdfbox.pdmodel.PDPageTree;
|
import org.apache.pdfbox.pdmodel.PDPageTree;
|
||||||
@@ -14,8 +15,10 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
|||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import io.github.pixee.security.Filenames;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.general.RotatePDFRequest;
|
import stirling.software.SPDF.model.api.general.RotatePDFRequest;
|
||||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
|
||||||
@@ -28,15 +31,15 @@ public class RotationController {
|
|||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/rotate-pdf")
|
@PostMapping(consumes = "multipart/form-data", value = "/rotate-pdf")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Rotate a PDF file",
|
summary = "Rotate a PDF file",
|
||||||
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"
|
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(
|
public ResponseEntity<byte[]> rotatePDF(@ModelAttribute RotatePDFRequest request)
|
||||||
@ModelAttribute RotatePDFRequest request) throws IOException {
|
throws IOException {
|
||||||
MultipartFile pdfFile = request.getFileInput();
|
MultipartFile pdfFile = request.getFileInput();
|
||||||
Integer angle = request.getAngle();
|
Integer angle = request.getAngle();
|
||||||
// Load the PDF document
|
// Load the PDF document
|
||||||
PDDocument document = PDDocument.load(pdfFile.getBytes());
|
PDDocument document = Loader.loadPDF(pdfFile.getBytes());
|
||||||
|
|
||||||
// Get the list of pages in the document
|
// Get the list of pages in the document
|
||||||
PDPageTree pages = document.getPages();
|
PDPageTree pages = document.getPages();
|
||||||
@@ -45,8 +48,10 @@ public class RotationController {
|
|||||||
page.setRotation(page.getRotation() + angle);
|
page.setRotation(page.getRotation() + angle);
|
||||||
}
|
}
|
||||||
|
|
||||||
return WebResponseUtils.pdfDocToWebResponse(document, pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_rotated.pdf");
|
return WebResponseUtils.pdfDocToWebResponse(
|
||||||
|
document,
|
||||||
|
Filenames.toSimpleFileName(pdfFile.getOriginalFilename())
|
||||||
|
.replaceFirst("[.][^.]+$", "")
|
||||||
|
+ "_rotated.pdf");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import java.io.IOException;
|
|||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.apache.pdfbox.Loader;
|
||||||
import org.apache.pdfbox.multipdf.LayerUtility;
|
import org.apache.pdfbox.multipdf.LayerUtility;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
import org.apache.pdfbox.pdmodel.PDPage;
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
@@ -21,90 +22,98 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
|||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import io.github.pixee.security.Filenames;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.general.ScalePagesRequest;
|
import stirling.software.SPDF.model.api.general.ScalePagesRequest;
|
||||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/general")
|
@RequestMapping("/api/v1/general")
|
||||||
@Tag(name = "General", description = "General APIs")
|
@Tag(name = "General", description = "General APIs")
|
||||||
public class ScalePagesController {
|
public class ScalePagesController {
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(ScalePagesController.class);
|
private static final Logger logger = LoggerFactory.getLogger(ScalePagesController.class);
|
||||||
|
|
||||||
@PostMapping(value = "/scale-pages", consumes = "multipart/form-data")
|
@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")
|
@Operation(
|
||||||
public ResponseEntity<byte[]> scalePages(@ModelAttribute ScalePagesRequest request) throws IOException {
|
summary = "Change the size of a PDF page/document",
|
||||||
MultipartFile file = request.getFileInput();
|
description =
|
||||||
String targetPDRectangle = request.getPageSize();
|
"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")
|
||||||
float scaleFactor = request.getScaleFactor();
|
public ResponseEntity<byte[]> scalePages(@ModelAttribute ScalePagesRequest request)
|
||||||
|
throws IOException {
|
||||||
|
MultipartFile file = request.getFileInput();
|
||||||
|
String targetPDRectangle = request.getPageSize();
|
||||||
|
float scaleFactor = request.getScaleFactor();
|
||||||
|
|
||||||
Map<String, PDRectangle> sizeMap = new HashMap<>();
|
Map<String, PDRectangle> sizeMap = new HashMap<>();
|
||||||
// Add A0 - A10
|
// Add A0 - A10
|
||||||
sizeMap.put("A0", PDRectangle.A0);
|
sizeMap.put("A0", PDRectangle.A0);
|
||||||
sizeMap.put("A1", PDRectangle.A1);
|
sizeMap.put("A1", PDRectangle.A1);
|
||||||
sizeMap.put("A2", PDRectangle.A2);
|
sizeMap.put("A2", PDRectangle.A2);
|
||||||
sizeMap.put("A3", PDRectangle.A3);
|
sizeMap.put("A3", PDRectangle.A3);
|
||||||
sizeMap.put("A4", PDRectangle.A4);
|
sizeMap.put("A4", PDRectangle.A4);
|
||||||
sizeMap.put("A5", PDRectangle.A5);
|
sizeMap.put("A5", PDRectangle.A5);
|
||||||
sizeMap.put("A6", PDRectangle.A6);
|
sizeMap.put("A6", PDRectangle.A6);
|
||||||
|
|
||||||
// Add other sizes
|
// Add other sizes
|
||||||
sizeMap.put("LETTER", PDRectangle.LETTER);
|
sizeMap.put("LETTER", PDRectangle.LETTER);
|
||||||
sizeMap.put("LEGAL", PDRectangle.LEGAL);
|
sizeMap.put("LEGAL", PDRectangle.LEGAL);
|
||||||
|
|
||||||
if (!sizeMap.containsKey(targetPDRectangle)) {
|
if (!sizeMap.containsKey(targetPDRectangle)) {
|
||||||
throw new IllegalArgumentException(
|
throw new IllegalArgumentException(
|
||||||
"Invalid PDRectangle. 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");
|
||||||
}
|
}
|
||||||
|
|
||||||
PDRectangle targetSize = sizeMap.get(targetPDRectangle);
|
PDRectangle targetSize = sizeMap.get(targetPDRectangle);
|
||||||
|
|
||||||
PDDocument sourceDocument = PDDocument.load(file.getBytes());
|
PDDocument sourceDocument = Loader.loadPDF(file.getBytes());
|
||||||
PDDocument outputDocument = new PDDocument();
|
PDDocument outputDocument = new PDDocument();
|
||||||
|
|
||||||
int totalPages = sourceDocument.getNumberOfPages();
|
int totalPages = sourceDocument.getNumberOfPages();
|
||||||
for (int i = 0; i < totalPages; i++) {
|
for (int i = 0; i < totalPages; i++) {
|
||||||
PDPage sourcePage = sourceDocument.getPage(i);
|
PDPage sourcePage = sourceDocument.getPage(i);
|
||||||
PDRectangle sourceSize = sourcePage.getMediaBox();
|
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);
|
|
||||||
|
|
||||||
contentStream.restoreGraphicsState();
|
float scaleWidth = targetSize.getWidth() / sourceSize.getWidth();
|
||||||
contentStream.close();
|
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,
|
||||||
|
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));
|
||||||
|
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
LayerUtility layerUtility = new LayerUtility(outputDocument);
|
||||||
outputDocument.save(baos);
|
PDFormXObject form = layerUtility.importPageAsForm(sourceDocument, i);
|
||||||
outputDocument.close();
|
contentStream.drawForm(form);
|
||||||
sourceDocument.close();
|
|
||||||
|
|
||||||
|
|
||||||
return WebResponseUtils.bytesToWebResponse(baos.toByteArray(),
|
|
||||||
file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_scaled.pdf");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
contentStream.restoreGraphicsState();
|
||||||
|
contentStream.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
outputDocument.save(baos);
|
||||||
|
outputDocument.close();
|
||||||
|
sourceDocument.close();
|
||||||
|
|
||||||
|
return WebResponseUtils.bytesToWebResponse(
|
||||||
|
baos.toByteArray(),
|
||||||
|
Filenames.toSimpleFileName(file.getOriginalFilename()).replaceFirst("[.][^.]+$", "")
|
||||||
|
+ "_scaled.pdf");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package stirling.software.SPDF.controller.api;
|
|||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -11,6 +10,7 @@ import java.util.stream.Collectors;
|
|||||||
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipEntry;
|
||||||
import java.util.zip.ZipOutputStream;
|
import java.util.zip.ZipOutputStream;
|
||||||
|
|
||||||
|
import org.apache.pdfbox.Loader;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
import org.apache.pdfbox.pdmodel.PDPage;
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@@ -23,8 +23,10 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
|||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import io.github.pixee.security.Filenames;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.PDFWithPageNums;
|
import stirling.software.SPDF.model.api.PDFWithPageNums;
|
||||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
|
||||||
@@ -36,19 +38,24 @@ public class SplitPDFController {
|
|||||||
private static final Logger logger = LoggerFactory.getLogger(SplitPDFController.class);
|
private static final Logger logger = LoggerFactory.getLogger(SplitPDFController.class);
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/split-pages")
|
@PostMapping(consumes = "multipart/form-data", value = "/split-pages")
|
||||||
@Operation(summary = "Split a PDF file into separate documents",
|
@Operation(
|
||||||
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")
|
summary = "Split a PDF file into separate documents",
|
||||||
public ResponseEntity<byte[]> splitPdf(@ModelAttribute PDFWithPageNums request) throws IOException {
|
description =
|
||||||
MultipartFile file = request.getFileInput();
|
"This endpoint splits a given PDF file into separate documents based on the specified page numbers or ranges. Users can specify pages using individual numbers, ranges, or 'all' for every page. Input:PDF Output:PDF Type:SIMO")
|
||||||
|
public ResponseEntity<byte[]> splitPdf(@ModelAttribute PDFWithPageNums request)
|
||||||
|
throws IOException {
|
||||||
|
MultipartFile file = request.getFileInput();
|
||||||
String pages = request.getPageNumbers();
|
String pages = request.getPageNumbers();
|
||||||
// open the pdf document
|
// open the pdf document
|
||||||
InputStream inputStream = file.getInputStream();
|
|
||||||
PDDocument document = PDDocument.load(inputStream);
|
|
||||||
|
|
||||||
List<Integer> pageNumbers = request.getPageNumbersList(document);
|
PDDocument document = Loader.loadPDF(file.getBytes());
|
||||||
if(!pageNumbers.contains(document.getNumberOfPages() - 1))
|
|
||||||
pageNumbers.add(document.getNumberOfPages()- 1);
|
List<Integer> pageNumbers = request.getPageNumbersList(document, true);
|
||||||
logger.info("Splitting PDF into pages: {}", pageNumbers.stream().map(String::valueOf).collect(Collectors.joining(",")));
|
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
|
// split the document
|
||||||
List<ByteArrayOutputStream> splitDocumentsBoas = new ArrayList<>();
|
List<ByteArrayOutputStream> splitDocumentsBoas = new ArrayList<>();
|
||||||
@@ -72,13 +79,14 @@ public class SplitPDFController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// closing the original document
|
// closing the original document
|
||||||
document.close();
|
document.close();
|
||||||
|
|
||||||
Path zipFile = Files.createTempFile("split_documents", ".zip");
|
Path zipFile = Files.createTempFile("split_documents", ".zip");
|
||||||
|
|
||||||
String filename = file.getOriginalFilename().replaceFirst("[.][^.]+$", "");
|
String filename =
|
||||||
|
Filenames.toSimpleFileName(file.getOriginalFilename())
|
||||||
|
.replaceFirst("[.][^.]+$", "");
|
||||||
try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(zipFile))) {
|
try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(zipFile))) {
|
||||||
// loop through the split documents and write them to the zip file
|
// loop through the split documents and write them to the zip file
|
||||||
for (int i = 0; i < splitDocumentsBoas.size(); i++) {
|
for (int i = 0; i < splitDocumentsBoas.size(); i++) {
|
||||||
@@ -104,8 +112,7 @@ public class SplitPDFController {
|
|||||||
Files.delete(zipFile);
|
Files.delete(zipFile);
|
||||||
|
|
||||||
// return the Resource in the response
|
// return the Resource in the response
|
||||||
return WebResponseUtils.bytesToWebResponse(data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM);
|
return WebResponseUtils.bytesToWebResponse(
|
||||||
|
data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,148 @@
|
|||||||
|
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.Loader;
|
||||||
|
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.PDPageContentStream.AppendMode;
|
||||||
|
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.github.pixee.security.Filenames;
|
||||||
|
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 = Loader.loadPDF(file.getBytes());
|
||||||
|
|
||||||
|
// 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 =
|
||||||
|
Filenames.toSimpleFileName(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, AppendMode.APPEND, true, true)) {
|
||||||
|
// Set clipping area and position
|
||||||
|
float translateX = -subPageWidth * i;
|
||||||
|
|
||||||
|
// float translateY = height - subPageHeight * (verticalDivisions - j);
|
||||||
|
float translateY = -subPageHeight * (verticalDivisions - 1 - 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
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.Loader;
|
||||||
|
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.github.pixee.security.Filenames;
|
||||||
|
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 = Loader.loadPDF(file.getBytes());
|
||||||
|
|
||||||
|
// 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 =
|
||||||
|
Filenames.toSimpleFileName(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import java.awt.geom.AffineTransform;
|
|||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import org.apache.pdfbox.Loader;
|
||||||
import org.apache.pdfbox.multipdf.LayerUtility;
|
import org.apache.pdfbox.multipdf.LayerUtility;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
import org.apache.pdfbox.pdmodel.PDPage;
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
@@ -20,8 +21,10 @@ import org.springframework.web.bind.annotation.RestController;
|
|||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.PDFFile;
|
import stirling.software.SPDF.model.api.PDFFile;
|
||||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/general")
|
@RequestMapping("/api/v1/general")
|
||||||
@Tag(name = "General", description = "General APIs")
|
@Tag(name = "General", description = "General APIs")
|
||||||
@@ -29,58 +32,61 @@ public class ToSinglePageController {
|
|||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(ToSinglePageController.class);
|
private static final Logger logger = LoggerFactory.getLogger(ToSinglePageController.class);
|
||||||
|
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/pdf-to-single-page")
|
@PostMapping(consumes = "multipart/form-data", value = "/pdf-to-single-page")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Convert a multi-page PDF into a single long page PDF",
|
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"
|
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(@ModelAttribute PDFFile request) throws IOException {
|
public ResponseEntity<byte[]> pdfToSinglePage(@ModelAttribute PDFFile request)
|
||||||
|
throws IOException {
|
||||||
|
|
||||||
// Load the source document
|
// Load the source document
|
||||||
PDDocument sourceDocument = PDDocument.load(request.getFileInput().getInputStream());
|
PDDocument sourceDocument = Loader.loadPDF(request.getFileInput().getBytes());
|
||||||
|
|
||||||
// Calculate total height and max width
|
// Calculate total height and max width
|
||||||
float totalHeight = 0;
|
float totalHeight = 0;
|
||||||
float maxWidth = 0;
|
float maxWidth = 0;
|
||||||
for (PDPage page : sourceDocument.getPages()) {
|
for (PDPage page : sourceDocument.getPages()) {
|
||||||
PDRectangle pageSize = page.getMediaBox();
|
PDRectangle pageSize = page.getMediaBox();
|
||||||
totalHeight += pageSize.getHeight();
|
totalHeight += pageSize.getHeight();
|
||||||
maxWidth = Math.max(maxWidth, pageSize.getWidth());
|
maxWidth = Math.max(maxWidth, pageSize.getWidth());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create new document and page with calculated dimensions
|
// Create new document and page with calculated dimensions
|
||||||
PDDocument newDocument = new PDDocument();
|
PDDocument newDocument = new PDDocument();
|
||||||
PDPage newPage = new PDPage(new PDRectangle(maxWidth, totalHeight));
|
PDPage newPage = new PDPage(new PDRectangle(maxWidth, totalHeight));
|
||||||
newDocument.addPage(newPage);
|
newDocument.addPage(newPage);
|
||||||
|
|
||||||
// Initialize the content stream of the new page
|
// Initialize the content stream of the new page
|
||||||
PDPageContentStream contentStream = new PDPageContentStream(newDocument, newPage);
|
PDPageContentStream contentStream = new PDPageContentStream(newDocument, newPage);
|
||||||
contentStream.close();
|
contentStream.close();
|
||||||
|
|
||||||
LayerUtility layerUtility = new LayerUtility(newDocument);
|
|
||||||
float yOffset = totalHeight;
|
|
||||||
|
|
||||||
// For each page, copy its content to the new page at the correct offset
|
LayerUtility layerUtility = new LayerUtility(newDocument);
|
||||||
for (PDPage page : sourceDocument.getPages()) {
|
float yOffset = totalHeight;
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
// For each page, copy its content to the new page at the correct offset
|
||||||
newDocument.save(baos);
|
for (PDPage page : sourceDocument.getPages()) {
|
||||||
newDocument.close();
|
PDFormXObject form =
|
||||||
sourceDocument.close();
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
byte[] result = baos.toByteArray();
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
return WebResponseUtils.bytesToWebResponse(result, request.getFileInput().getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_singlePage.pdf");
|
newDocument.save(baos);
|
||||||
|
newDocument.close();
|
||||||
|
sourceDocument.close();
|
||||||
|
|
||||||
|
byte[] result = baos.toByteArray();
|
||||||
|
return WebResponseUtils.bytesToWebResponse(
|
||||||
|
result,
|
||||||
|
request.getFileInput().getOriginalFilename().replaceFirst("[.][^.]+$", "")
|
||||||
|
+ "_singlePage.pdf");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import org.springframework.security.core.Authentication;
|
|||||||
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
|
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.ui.Model;
|
import org.springframework.ui.Model;
|
||||||
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
@@ -20,60 +21,72 @@ import org.springframework.web.bind.annotation.RequestParam;
|
|||||||
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
|
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
|
||||||
import org.springframework.web.servlet.view.RedirectView;
|
import org.springframework.web.servlet.view.RedirectView;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import stirling.software.SPDF.config.security.UserService;
|
import stirling.software.SPDF.config.security.UserService;
|
||||||
|
import stirling.software.SPDF.model.Role;
|
||||||
import stirling.software.SPDF.model.User;
|
import stirling.software.SPDF.model.User;
|
||||||
|
import stirling.software.SPDF.model.api.user.UpdateUserDetails;
|
||||||
|
import stirling.software.SPDF.model.api.user.UsernameAndPass;
|
||||||
|
|
||||||
@Controller
|
@Controller
|
||||||
|
@Tag(name = "User", description = "User APIs")
|
||||||
@RequestMapping("/api/v1/user")
|
@RequestMapping("/api/v1/user")
|
||||||
public class UserController {
|
public class UserController {
|
||||||
|
|
||||||
@Autowired
|
@Autowired private UserService userService;
|
||||||
private UserService userService;
|
|
||||||
|
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
||||||
@PostMapping("/register")
|
@PostMapping("/register")
|
||||||
public String register(@RequestParam String username, @RequestParam String password, Model model) {
|
public String register(@ModelAttribute UsernameAndPass requestModel, Model model) {
|
||||||
if(userService.usernameExists(username)) {
|
if (userService.usernameExists(requestModel.getUsername())) {
|
||||||
model.addAttribute("error", "Username already exists");
|
model.addAttribute("error", "Username already exists");
|
||||||
return "register";
|
return "register";
|
||||||
}
|
}
|
||||||
|
|
||||||
userService.saveUser(username, password);
|
userService.saveUser(requestModel.getUsername(), requestModel.getPassword());
|
||||||
return "redirect:/login?registered=true";
|
return "redirect:/login?registered=true";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
||||||
@PostMapping("/change-username-and-password")
|
@PostMapping("/change-username-and-password")
|
||||||
public RedirectView changeUsernameAndPassword(Principal principal,
|
public RedirectView changeUsernameAndPassword(
|
||||||
@RequestParam String currentPassword,
|
Principal principal,
|
||||||
@RequestParam String newUsername,
|
@ModelAttribute UpdateUserDetails requestModel,
|
||||||
@RequestParam String newPassword,
|
HttpServletRequest request,
|
||||||
HttpServletRequest request,
|
HttpServletResponse response,
|
||||||
HttpServletResponse response,
|
RedirectAttributes redirectAttributes) {
|
||||||
RedirectAttributes redirectAttributes) {
|
|
||||||
if (principal == null) {
|
|
||||||
return new RedirectView("/change-creds?messageType=notAuthenticated");
|
|
||||||
}
|
|
||||||
|
|
||||||
Optional<User> userOpt = userService.findByUsername(principal.getName());
|
String currentPassword = requestModel.getPassword();
|
||||||
|
String newPassword = requestModel.getNewPassword();
|
||||||
|
String newUsername = requestModel.getNewUsername();
|
||||||
|
|
||||||
if (userOpt == null || userOpt.isEmpty()) {
|
if (principal == null) {
|
||||||
return new RedirectView("/change-creds?messageType=userNotFound");
|
return new RedirectView("/change-creds?messageType=notAuthenticated");
|
||||||
}
|
}
|
||||||
|
|
||||||
User user = userOpt.get();
|
Optional<User> userOpt = userService.findByUsername(principal.getName());
|
||||||
|
|
||||||
if (!userService.isPasswordCorrect(user, currentPassword)) {
|
if (userOpt == null || userOpt.isEmpty()) {
|
||||||
return new RedirectView("/change-creds?messageType=incorrectPassword");
|
return new RedirectView("/change-creds?messageType=userNotFound");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user.getUsername().equals(newUsername) && userService.usernameExists(newUsername)) {
|
User user = userOpt.get();
|
||||||
return new RedirectView("/change-creds?messageType=usernameExists");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
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);
|
userService.changePassword(user, newPassword);
|
||||||
if(newUsername != null && newUsername.length() > 0 && !user.getUsername().equals(newUsername)) {
|
if (newUsername != null
|
||||||
|
&& newUsername.length() > 0
|
||||||
|
&& !user.getUsername().equals(newUsername)) {
|
||||||
userService.changeUsername(user, newUsername);
|
userService.changeUsername(user, newUsername);
|
||||||
}
|
}
|
||||||
userService.changeFirstUse(user, false);
|
userService.changeFirstUse(user, false);
|
||||||
@@ -84,36 +97,36 @@ public class UserController {
|
|||||||
return new RedirectView("/login?messageType=credsUpdated");
|
return new RedirectView("/login?messageType=credsUpdated");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
||||||
|
|
||||||
@PostMapping("/change-username")
|
@PostMapping("/change-username")
|
||||||
public RedirectView changeUsername(Principal principal,
|
public RedirectView changeUsername(
|
||||||
@RequestParam String currentPassword,
|
Principal principal,
|
||||||
@RequestParam String newUsername,
|
@RequestParam String currentPassword,
|
||||||
HttpServletRequest request,
|
@RequestParam String newUsername,
|
||||||
HttpServletResponse response,
|
HttpServletRequest request,
|
||||||
RedirectAttributes redirectAttributes) {
|
HttpServletResponse response,
|
||||||
if (principal == null) {
|
RedirectAttributes redirectAttributes) {
|
||||||
return new RedirectView("/account?messageType=notAuthenticated");
|
if (principal == null) {
|
||||||
}
|
return new RedirectView("/account?messageType=notAuthenticated");
|
||||||
|
}
|
||||||
|
|
||||||
Optional<User> userOpt = userService.findByUsername(principal.getName());
|
Optional<User> userOpt = userService.findByUsername(principal.getName());
|
||||||
|
|
||||||
if (userOpt == null || userOpt.isEmpty()) {
|
if (userOpt == null || userOpt.isEmpty()) {
|
||||||
return new RedirectView("/account?messageType=userNotFound");
|
return new RedirectView("/account?messageType=userNotFound");
|
||||||
}
|
}
|
||||||
|
|
||||||
User user = userOpt.get();
|
User user = userOpt.get();
|
||||||
|
|
||||||
if (!userService.isPasswordCorrect(user, currentPassword)) {
|
if (!userService.isPasswordCorrect(user, currentPassword)) {
|
||||||
return new RedirectView("/account?messageType=incorrectPassword");
|
return new RedirectView("/account?messageType=incorrectPassword");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user.getUsername().equals(newUsername) && userService.usernameExists(newUsername)) {
|
if (!user.getUsername().equals(newUsername) && userService.usernameExists(newUsername)) {
|
||||||
return new RedirectView("/account?messageType=usernameExists");
|
return new RedirectView("/account?messageType=usernameExists");
|
||||||
}
|
}
|
||||||
|
|
||||||
if(newUsername != null && newUsername.length() > 0) {
|
if (newUsername != null && newUsername.length() > 0) {
|
||||||
userService.changeUsername(user, newUsername);
|
userService.changeUsername(user, newUsername);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,28 +136,30 @@ public class UserController {
|
|||||||
return new RedirectView("/login?messageType=credsUpdated");
|
return new RedirectView("/login?messageType=credsUpdated");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
||||||
@PostMapping("/change-password")
|
@PostMapping("/change-password")
|
||||||
public RedirectView changePassword(Principal principal,
|
public RedirectView changePassword(
|
||||||
@RequestParam String currentPassword,
|
Principal principal,
|
||||||
@RequestParam String newPassword,
|
@RequestParam String currentPassword,
|
||||||
HttpServletRequest request,
|
@RequestParam String newPassword,
|
||||||
HttpServletResponse response,
|
HttpServletRequest request,
|
||||||
RedirectAttributes redirectAttributes) {
|
HttpServletResponse response,
|
||||||
if (principal == null) {
|
RedirectAttributes redirectAttributes) {
|
||||||
return new RedirectView("/account?messageType=notAuthenticated");
|
if (principal == null) {
|
||||||
}
|
return new RedirectView("/account?messageType=notAuthenticated");
|
||||||
|
}
|
||||||
|
|
||||||
Optional<User> userOpt = userService.findByUsername(principal.getName());
|
Optional<User> userOpt = userService.findByUsername(principal.getName());
|
||||||
|
|
||||||
if (userOpt == null || userOpt.isEmpty()) {
|
if (userOpt == null || userOpt.isEmpty()) {
|
||||||
return new RedirectView("/account?messageType=userNotFound");
|
return new RedirectView("/account?messageType=userNotFound");
|
||||||
}
|
}
|
||||||
|
|
||||||
User user = userOpt.get();
|
User user = userOpt.get();
|
||||||
|
|
||||||
if (!userService.isPasswordCorrect(user, currentPassword)) {
|
if (!userService.isPasswordCorrect(user, currentPassword)) {
|
||||||
return new RedirectView("/account?messageType=incorrectPassword");
|
return new RedirectView("/account?messageType=incorrectPassword");
|
||||||
}
|
}
|
||||||
|
|
||||||
userService.changePassword(user, newPassword);
|
userService.changePassword(user, newPassword);
|
||||||
|
|
||||||
@@ -154,55 +169,71 @@ public class UserController {
|
|||||||
return new RedirectView("/login?messageType=credsUpdated");
|
return new RedirectView("/login?messageType=credsUpdated");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
||||||
@PostMapping("/updateUserSettings")
|
@PostMapping("/updateUserSettings")
|
||||||
public String updateUserSettings(HttpServletRequest request, Principal principal) {
|
public String updateUserSettings(HttpServletRequest request, Principal principal) {
|
||||||
Map<String, String[]> paramMap = request.getParameterMap();
|
Map<String, String[]> paramMap = request.getParameterMap();
|
||||||
Map<String, String> updates = new HashMap<>();
|
Map<String, String> updates = new HashMap<>();
|
||||||
|
|
||||||
System.out.println("Received parameter map: " + paramMap);
|
System.out.println("Received parameter map: " + paramMap);
|
||||||
|
|
||||||
for (Map.Entry<String, String[]> entry : paramMap.entrySet()) {
|
for (Map.Entry<String, String[]> entry : paramMap.entrySet()) {
|
||||||
updates.put(entry.getKey(), entry.getValue()[0]);
|
updates.put(entry.getKey(), entry.getValue()[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
System.out.println("Processed updates: " + updates);
|
System.out.println("Processed updates: " + updates);
|
||||||
|
|
||||||
// Assuming you have a method in userService to update the settings for a user
|
// Assuming you have a method in userService to update the settings for a user
|
||||||
userService.updateUserSettings(principal.getName(), updates);
|
userService.updateUserSettings(principal.getName(), updates);
|
||||||
|
|
||||||
return "redirect:/account"; // Redirect to a page of your choice after updating
|
return "redirect:/account"; // Redirect to a page of your choice after updating
|
||||||
}
|
}
|
||||||
|
|
||||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||||
@PostMapping("/admin/saveUser")
|
@PostMapping("/admin/saveUser")
|
||||||
public RedirectView saveUser(@RequestParam String username, @RequestParam String password, @RequestParam String role,
|
public RedirectView saveUser(
|
||||||
@RequestParam(name = "forceChange", required = false, defaultValue = "false") boolean forceChange) {
|
@RequestParam String username,
|
||||||
|
@RequestParam String password,
|
||||||
if(userService.usernameExists(username)) {
|
@RequestParam String role,
|
||||||
return new RedirectView("/addUsers?messageType=usernameExists");
|
@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);
|
userService.saveUser(username, password, role, forceChange);
|
||||||
return new RedirectView("/addUsers"); // Redirect to account page after adding the user
|
return new RedirectView("/addUsers"); // Redirect to account page after adding the user
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||||
@PostMapping("/admin/deleteUser/{username}")
|
@PostMapping("/admin/deleteUser/{username}")
|
||||||
public String deleteUser(@PathVariable String username, Authentication authentication) {
|
public String deleteUser(@PathVariable String username, Authentication authentication) {
|
||||||
|
|
||||||
// Get the currently authenticated username
|
// Get the currently authenticated username
|
||||||
String currentUsername = authentication.getName();
|
String currentUsername = authentication.getName();
|
||||||
|
|
||||||
// Check if the provided username matches the current session's username
|
// Check if the provided username matches the current session's username
|
||||||
if (currentUsername.equals(username)) {
|
if (currentUsername.equals(username)) {
|
||||||
throw new IllegalArgumentException("Cannot delete currently logined in user.");
|
throw new IllegalArgumentException("Cannot delete currently logined in user.");
|
||||||
}
|
}
|
||||||
|
|
||||||
userService.deleteUser(username);
|
userService.deleteUser(username);
|
||||||
return "redirect:/addUsers";
|
return "redirect:/addUsers";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
||||||
@PostMapping("/get-api-key")
|
@PostMapping("/get-api-key")
|
||||||
public ResponseEntity<String> getApiKey(Principal principal) {
|
public ResponseEntity<String> getApiKey(Principal principal) {
|
||||||
if (principal == null) {
|
if (principal == null) {
|
||||||
@@ -216,6 +247,7 @@ public class UserController {
|
|||||||
return ResponseEntity.ok(apiKey);
|
return ResponseEntity.ok(apiKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
||||||
@PostMapping("/update-api-key")
|
@PostMapping("/update-api-key")
|
||||||
public ResponseEntity<String> updateApiKey(Principal principal) {
|
public ResponseEntity<String> updateApiKey(Principal principal) {
|
||||||
if (principal == null) {
|
if (principal == null) {
|
||||||
@@ -229,6 +261,4 @@ public class UserController {
|
|||||||
}
|
}
|
||||||
return ResponseEntity.ok(apiKey);
|
return ResponseEntity.ok(apiKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user