Compare commits
793 Commits
v0.9.1
...
add-elemen
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
217bba3a9d | ||
|
|
7572db9bd4 | ||
|
|
9e3b50dff3 | ||
|
|
cf640c7e3f | ||
|
|
ec770e1008 | ||
|
|
15e0048bfc | ||
|
|
b572a5e4c9 | ||
|
|
c55a5657a4 | ||
|
|
5f771b7851 | ||
|
|
b58cbdcb61 | ||
|
|
9e81667ecd | ||
|
|
a92479b505 | ||
|
|
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 | ||
|
|
0e9f52bca2 | ||
|
|
44e3556382 | ||
|
|
4f6286845d | ||
|
|
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 | ||
|
|
5d611a2fa3 | ||
|
|
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 | ||
|
|
86984f2142 | ||
|
|
bc4640c3f0 | ||
|
|
1e2eb9b07a | ||
|
|
ece00956d9 | ||
|
|
af5bbd8838 | ||
|
|
3a8f2495ea | ||
|
|
993f5e5097 | ||
|
|
1f99c26e78 | ||
|
|
05ebf3a6b4 | ||
|
|
1be3046d26 | ||
|
|
5b3858ba29 | ||
|
|
a1f388e524 | ||
|
|
cf14ff1540 | ||
|
|
a0ac2bc02a | ||
|
|
ed82c492ab | ||
|
|
fc2d71d120 | ||
|
|
e5a7a0631b | ||
|
|
315cba07f0 | ||
|
|
cd5bd92a41 | ||
|
|
10126ce979 | ||
|
|
f6c4f08254 | ||
|
|
fc4feb2096 | ||
|
|
82b641458f | ||
|
|
2c0fb33548 | ||
|
|
24e665bfd5 | ||
|
|
1404e33386 | ||
|
|
0b1fd61188 | ||
|
|
80b4c67e35 | ||
|
|
1f4aae9249 | ||
|
|
145e8006f0 | ||
|
|
872f562aad | ||
|
|
db70d67180 | ||
|
|
b1fd798a5c | ||
|
|
42907ade21 | ||
|
|
b3bc0b4e5a | ||
|
|
12e24f3ec1 | ||
|
|
fefa8347da | ||
|
|
cd7ca09a3f | ||
|
|
3960b2d6f9 | ||
|
|
264969e80c | ||
|
|
4e2911f648 | ||
|
|
42610b2645 | ||
|
|
7b07a706e1 | ||
|
|
8fef9dc8a8 | ||
|
|
83936bf4c8 | ||
|
|
65f89e283e | ||
|
|
1aa65bd3d1 | ||
|
|
0547ec3c49 | ||
|
|
7e1cbe572d | ||
|
|
7a98f30d05 | ||
|
|
c565bd4400 | ||
|
|
d039c8e62e | ||
|
|
da7f0561cb | ||
|
|
00210b75c7 | ||
|
|
3fe8973ae6 | ||
|
|
87c4b42683 | ||
|
|
0901eaac45 | ||
|
|
e998426b3b | ||
|
|
c7c81a7243 | ||
|
|
ac019ac196 | ||
|
|
b12c1305ea | ||
|
|
1876c2afae | ||
|
|
4575375ea9 | ||
|
|
aac7836dce | ||
|
|
df5de2d60d | ||
|
|
b026283ef2 | ||
|
|
548e5d108c | ||
|
|
4d31152a02 | ||
|
|
fd08513212 | ||
|
|
3175ac16d1 | ||
|
|
0cb5a6c7ad | ||
|
|
0bb2df135b | ||
|
|
adadf7428c | ||
|
|
146dd3c00b | ||
|
|
78bfa84afd | ||
|
|
07512c7e2c | ||
|
|
9fe96bec40 | ||
|
|
18172aa33a | ||
|
|
9ece6dacbd | ||
|
|
ef07963d79 | ||
|
|
862086eae5 | ||
|
|
277aa0c7e1 | ||
|
|
fc52741435 | ||
|
|
a7cd6bfd2e | ||
|
|
648df7b3d3 | ||
|
|
01f7f1f59c | ||
|
|
ff7f089f69 | ||
|
|
4e06e8c0c0 | ||
|
|
ca5ce905c7 | ||
|
|
0fc29de02c | ||
|
|
8509a16d6e | ||
|
|
0ed021357b | ||
|
|
58ad7a1e8a | ||
|
|
622ee29dd8 | ||
|
|
5cbb4c6223 | ||
|
|
5299859385 | ||
|
|
c57b308909 | ||
|
|
6409274f83 | ||
|
|
bc534c12a5 | ||
|
|
b58fd2022a | ||
|
|
d850d026ed | ||
|
|
83627686d4 | ||
|
|
4e7d01c72c | ||
|
|
0f8ab20db7 | ||
|
|
88cc90786d | ||
|
|
fb66717b43 | ||
|
|
1d60433fcf | ||
|
|
0f3df6e92b | ||
|
|
ca7c63c7d7 | ||
|
|
135f9611df | ||
|
|
cfaaeebd4a | ||
|
|
09a0779180 | ||
|
|
7f7d09bc85 | ||
|
|
0c454a08dc | ||
|
|
d749b63549 | ||
|
|
2053a6950d | ||
|
|
41bd801e0d | ||
|
|
cd0e1a3962 | ||
|
|
7c2f482b3b | ||
|
|
7741d60afd | ||
|
|
8aac0c0327 | ||
|
|
7c26c56210 | ||
|
|
e88a780efe | ||
|
|
363fb5dc02 | ||
|
|
39a187b6da | ||
|
|
b4cc34a522 | ||
|
|
af94ef3d49 | ||
|
|
505855a53c | ||
|
|
87ac245341 | ||
|
|
1670a09d04 | ||
|
|
620b954336 | ||
|
|
cfd51e9b84 | ||
|
|
6c797f8216 | ||
|
|
40e208152a | ||
|
|
cf7bfa62ef | ||
|
|
a1086b9a04 | ||
|
|
9bc4bbd2c8 | ||
|
|
73156012e9 | ||
|
|
91cc3d77d4 | ||
|
|
3fc55a9e9f | ||
|
|
b666aa3f26 | ||
|
|
53e7dbe12f | ||
|
|
9d8ff6856b | ||
|
|
3fb38376b0 | ||
|
|
15c73d9dd3 | ||
|
|
86f71ffb93 | ||
|
|
eb928d3369 | ||
|
|
d7307665b3 | ||
|
|
2836f0ab5a | ||
|
|
91b7f3980c | ||
|
|
5053432c2d | ||
|
|
563c612395 | ||
|
|
95dced6455 | ||
|
|
989f0bbbfb | ||
|
|
e5eec28bfd | ||
|
|
bfc402f307 | ||
|
|
35a998b934 | ||
|
|
cadc8e499d | ||
|
|
7f7ea6da9f | ||
|
|
ab9a22d8e7 | ||
|
|
cd2728105e | ||
|
|
d75e84bdff | ||
|
|
e791fee38b | ||
|
|
ad5f057733 | ||
|
|
6f325b5fdb | ||
|
|
b73aeee18d | ||
|
|
8a54035a9f | ||
|
|
af28e30e4c | ||
|
|
492513306c | ||
|
|
bc554ff4a7 | ||
|
|
b7a0d1ece8 | ||
|
|
ca384218dc | ||
|
|
e9550fd6b2 | ||
|
|
232a305d51 | ||
|
|
a6ab448eeb | ||
|
|
f9aa157c6c | ||
|
|
b7df24acaa | ||
|
|
ad4ca1b2d7 | ||
|
|
c562d197e7 | ||
|
|
83ba1899b7 | ||
|
|
3420adc7c9 | ||
|
|
fd39f28e46 | ||
|
|
710125852a | ||
|
|
e8ec208390 | ||
|
|
4584562607 | ||
|
|
2c1412a088 | ||
|
|
0a65382979 | ||
|
|
4f404f66e5 | ||
|
|
89505ada00 | ||
|
|
374a30ac5a | ||
|
|
cee4ee4128 | ||
|
|
58fc5e2ffa | ||
|
|
951ea43f8b | ||
|
|
ca16ecef24 | ||
|
|
bb025dc2a1 | ||
|
|
d797169bd0 | ||
|
|
891f9e2252 | ||
|
|
4a579c00ce | ||
|
|
9cb4d8e088 | ||
|
|
5d3ee7755a | ||
|
|
c047c46587 | ||
|
|
54f53be5b5 | ||
|
|
8bb9e5b22f | ||
|
|
379791a326 | ||
|
|
38ec68b303 | ||
|
|
a5095b04ad | ||
|
|
a27ddb40be | ||
|
|
bc36be8a5e | ||
|
|
1e35556034 | ||
|
|
882cd41d4b | ||
|
|
724fb4bf8f | ||
|
|
b07437dbfa | ||
|
|
96f05cd518 | ||
|
|
77411e94a4 | ||
|
|
0da9c62ef8 | ||
|
|
52a7885f3c | ||
|
|
f98f089d63 | ||
|
|
6b618f3abe | ||
|
|
0732ffa76e | ||
|
|
b5b4636e56 | ||
|
|
ca12d040e1 | ||
|
|
3388b9fafa | ||
|
|
954b36e14c | ||
|
|
4d43814220 | ||
|
|
f6262c82e1 | ||
|
|
7ead12922f | ||
|
|
33a6a7869c | ||
|
|
bf995f989c | ||
|
|
21de6c6520 | ||
|
|
c14aa6851e | ||
|
|
8260eced2d | ||
|
|
d028465dc5 | ||
|
|
29dab5e47d | ||
|
|
9e655631b4 | ||
|
|
179c7b80bb | ||
|
|
349bf29122 | ||
|
|
295357f12b | ||
|
|
940f8d999e | ||
|
|
5605d53a5f | ||
|
|
116d103119 | ||
|
|
2fd8c643af | ||
|
|
4367ae7934 | ||
|
|
749461334d | ||
|
|
e83a027023 | ||
|
|
1883b477a3 | ||
|
|
37e2cd40da | ||
|
|
81a9329975 | ||
|
|
0eb019fc3c | ||
|
|
4129c75475 | ||
|
|
3d66f03f58 | ||
|
|
7b83104fd6 | ||
|
|
794aede27f | ||
|
|
08eb39b206 | ||
|
|
2566c7f3d7 | ||
|
|
a8522bb3b5 | ||
|
|
92b9142902 | ||
|
|
d07e3e6522 | ||
|
|
29aabdfba8 | ||
|
|
9af1b0cfdc | ||
|
|
6e32c7fe85 | ||
|
|
ddf5915c6a | ||
|
|
cdbf1fa73a | ||
|
|
5d926b022b | ||
|
|
50bcca10e2 | ||
|
|
a5528c06ee | ||
|
|
94526de04b | ||
|
|
1ddf7abe6f | ||
|
|
a742c1b034 | ||
|
|
6e726ac2a6 | ||
|
|
5877b40be5 | ||
|
|
a3c7f5aa46 | ||
|
|
4e28bf03bd | ||
|
|
f92482d89e | ||
|
|
3c54429fe0 | ||
|
|
4c4c22e861 | ||
|
|
00e27d9327 | ||
|
|
f082278041 | ||
|
|
09dde64c57 | ||
|
|
b52a6357f6 | ||
|
|
f14a566d06 | ||
|
|
b352ec6888 | ||
|
|
4ec1bad03d | ||
|
|
f20bbc119d | ||
|
|
8555c3d422 | ||
|
|
71f9d03b19 | ||
|
|
0b31379078 | ||
|
|
29c204b2c2 | ||
|
|
657e881963 | ||
|
|
ef6bdc70a4 | ||
|
|
e4a36115a2 | ||
|
|
325f86832c | ||
|
|
e3dbdd6b09 | ||
|
|
919041e879 | ||
|
|
182231a183 | ||
|
|
46d4ae8fc5 | ||
|
|
73ab1936a3 | ||
|
|
59c72527b5 | ||
|
|
5ea3bcc1dd | ||
|
|
c140052822 | ||
|
|
f7832774d9 | ||
|
|
279d25c03a | ||
|
|
f1984047a8 | ||
|
|
eae7c1bd60 | ||
|
|
d7f592ebda | ||
|
|
1798ce002a | ||
|
|
57a0cca595 | ||
|
|
b26fbd7693 | ||
|
|
43b0e25bdb | ||
|
|
3377af1305 | ||
|
|
c81c1006b7 | ||
|
|
f313857f96 | ||
|
|
d5fbe02149 | ||
|
|
159cee0b39 | ||
|
|
5da4dd6cca | ||
|
|
e9daf05f16 | ||
|
|
a12643194a | ||
|
|
25d8fc08f7 | ||
|
|
a72378dd4d | ||
|
|
9aed70408b | ||
|
|
5ae2c71c3a | ||
|
|
4edce515b8 | ||
|
|
67ff664eb8 | ||
|
|
71c1a4f102 | ||
|
|
ba4ba1b9fc | ||
|
|
59f10f06ca | ||
|
|
f7953cbc37 | ||
|
|
9a74e81754 | ||
|
|
420e4b6766 | ||
|
|
aed48ffc93 | ||
|
|
0cebe69ff8 | ||
|
|
e5990dba81 | ||
|
|
c9b0d01250 | ||
|
|
4918ed3f3c | ||
|
|
b176ce4251 | ||
|
|
518ff5409e | ||
|
|
803bd3c5b2 | ||
|
|
03d4e73304 | ||
|
|
55a820b09f | ||
|
|
f2a65dc360 | ||
|
|
7b4a889ea7 | ||
|
|
f627d251c3 | ||
|
|
d5b7125415 | ||
|
|
67dd3cf0e3 | ||
|
|
b8b62bb5af | ||
|
|
b4a9d1ac18 | ||
|
|
8aae651c2c | ||
|
|
fc9465b324 | ||
|
|
579a50be2c | ||
|
|
9c5b967e4c | ||
|
|
d41deb729b | ||
|
|
a93a89f3f0 | ||
|
|
11d642a25f | ||
|
|
67448498ea | ||
|
|
489b8da713 | ||
|
|
728d4d0fa8 | ||
|
|
e70f4ff3a6 | ||
|
|
04032c0dfe | ||
|
|
ecba6461df | ||
|
|
433ba6c250 | ||
|
|
ff6c55d1d0 | ||
|
|
d9f8facf2e | ||
|
|
6bd3e5cc8f | ||
|
|
936fb5ae45 | ||
|
|
8e661e1260 | ||
|
|
987d9b0502 | ||
|
|
f6a9169446 | ||
|
|
d5c1c43eb1 | ||
|
|
20c74dac60 | ||
|
|
30161275a3 | ||
|
|
7e9479806e | ||
|
|
39b9ea9f1d | ||
|
|
5a9165d7c6 | ||
|
|
4f851156b7 | ||
|
|
b8fa278173 | ||
|
|
2cfb344e13 | ||
|
|
1d2bf92abe | ||
|
|
cefcda9f40 | ||
|
|
a4bc67ff8e | ||
|
|
48b3dea256 | ||
|
|
1f5231d905 | ||
|
|
c526e18992 | ||
|
|
4594765cbd | ||
|
|
e2a787e519 | ||
|
|
45b3e0aa6a | ||
|
|
7bdb2615d4 | ||
|
|
019a502490 | ||
|
|
6793fd5bc7 | ||
|
|
8ac5cf759e | ||
|
|
19880567ba | ||
|
|
5c5a3fefc1 | ||
|
|
90057828a5 | ||
|
|
7cfc3a09a2 | ||
|
|
b8bbee27f8 | ||
|
|
8e5c665e49 | ||
|
|
7b1e6fb953 | ||
|
|
79936e69c5 | ||
|
|
a19cd327f3 | ||
|
|
5c831c156b | ||
|
|
602df08df5 | ||
|
|
3b8d06a9e3 | ||
|
|
5fd43b50e0 | ||
|
|
15d39413f3 | ||
|
|
5e01946981 | ||
|
|
576d11f09a | ||
|
|
a43c296eef | ||
|
|
6015591e34 | ||
|
|
6f5f031b24 | ||
|
|
1499e78795 | ||
|
|
1b45ab7222 | ||
|
|
005b158ad3 | ||
|
|
26d003e840 | ||
|
|
192fb39302 | ||
|
|
1650dfcc29 | ||
|
|
fda83c4c1d | ||
|
|
1a6ebbb8e5 | ||
|
|
ea1f8912b8 | ||
|
|
a5885d2628 | ||
|
|
716d4c6f28 | ||
|
|
b3a36c82bb | ||
|
|
75fff1d52f | ||
|
|
2d88987cb3 | ||
|
|
8bbbdbd359 | ||
|
|
8a2aa44de8 | ||
|
|
3715c555d3 | ||
|
|
5e4de6cc5f | ||
|
|
4cadfc64f6 | ||
|
|
a3f0d47cad | ||
|
|
bc55b5fdda | ||
|
|
43474712eb |
2
.git-blame-ignore-revs
Normal file
2
.git-blame-ignore-revs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# Formatting
|
||||||
|
5f771b785130154ed47952635b7acef371ffe0ec
|
||||||
1
.gitattributes
vendored
1
.gitattributes
vendored
@@ -1,5 +1,6 @@
|
|||||||
# 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/pull_request_template.md
vendored
Normal file
4
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# License Agreement for Contributions
|
||||||
|
By submitting this pull request, I acknowledge and agree that my contributions will be included in Stirling-PDF and that they can be relicensed in the future under MPL 2.0 (Mozilla Public License Version 2.0) license.
|
||||||
|
|
||||||
|
(This does not change the general open-source nature of Stirling-PDF, simply moving from one license to another license)
|
||||||
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
|
|
||||||
3
.github/workflows/pull_request_template.md
vendored
Normal file
3
.github/workflows/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# License Agreement for Contributions
|
||||||
|
By submitting this pull request, I acknowledge and agree that my contributions will be included in Stirling-PDF and that they can be relicensed in the future under MPL 2.0 (Mozilla Public License Version 2.0) license.
|
||||||
|
(This does not change the open-source nature of Stirling-PDF, simply moving from one license to another license)
|
||||||
65
.github/workflows/push-docker.yml
vendored
65
.github/workflows/push-docker.yml
vendored
@@ -8,7 +8,6 @@ on:
|
|||||||
- main
|
- main
|
||||||
jobs:
|
jobs:
|
||||||
push:
|
push:
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
|
||||||
@@ -22,6 +21,8 @@ jobs:
|
|||||||
|
|
||||||
|
|
||||||
- uses: gradle/gradle-build-action@v2.4.2
|
- uses: gradle/gradle-build-action@v2.4.2
|
||||||
|
env:
|
||||||
|
DOCKER_ENABLE_SECURITY: false
|
||||||
with:
|
with:
|
||||||
gradle-version: 7.6
|
gradle-version: 7.6
|
||||||
arguments: clean build
|
arguments: clean build
|
||||||
@@ -46,13 +47,17 @@ jobs:
|
|||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ github.token }}
|
password: ${{ github.token }}
|
||||||
|
|
||||||
|
- name: Convert repository owner to lowercase
|
||||||
|
id: repoowner
|
||||||
|
run: echo "::set-output name=lowercase::$(echo ${{ github.repository_owner }} | awk '{print tolower($0)}')"
|
||||||
|
|
||||||
- name: Generate tags
|
- name: Generate tags
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v4.4.0
|
uses: docker/metadata-action@v4.4.0
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
|
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
|
||||||
ghcr.io/${{ github.repository_owner }}/s-pdf
|
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/s-pdf
|
||||||
tags: |
|
tags: |
|
||||||
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }},enable=${{ github.ref == 'refs/heads/master' }}
|
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }},enable=${{ github.ref == 'refs/heads/master' }}
|
||||||
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/master' }}
|
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/master' }}
|
||||||
@@ -74,34 +79,70 @@ jobs:
|
|||||||
cache-to: type=gha,mode=max
|
cache-to: type=gha,mode=max
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
build-args:
|
||||||
|
VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
|
||||||
platforms: linux/amd64,linux/arm64/v8
|
platforms: linux/amd64,linux/arm64/v8
|
||||||
|
|
||||||
- name: Generate tags
|
|
||||||
|
|
||||||
|
- name: Generate tags ultra-lite
|
||||||
id: meta2
|
id: meta2
|
||||||
uses: docker/metadata-action@v4.4.0
|
uses: docker/metadata-action@v4.4.0
|
||||||
|
if: github.ref != 'refs/heads/main'
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
|
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
|
||||||
ghcr.io/${{ github.repository_owner }}/s-pdf
|
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/s-pdf
|
||||||
tags: |
|
tags: |
|
||||||
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}-ultra-light,enable=${{ github.ref == 'refs/heads/master' }}
|
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}-ultra-lite,enable=${{ github.ref == 'refs/heads/master' }}
|
||||||
type=raw,value=latest-ultra-light,enable=${{ github.ref == 'refs/heads/master' }}
|
type=raw,value=latest-ultra-lite,enable=${{ github.ref == 'refs/heads/master' }}
|
||||||
type=raw,value=alpha-ultra-light,enable=${{ github.ref == 'refs/heads/main' }}
|
|
||||||
|
|
||||||
- name: Convert repository owner to lowercase
|
|
||||||
id: repoowner
|
|
||||||
run: echo "::set-output name=lowercase::$(echo ${{ github.repository_owner }} | awk '{print tolower($0)}')"
|
|
||||||
|
|
||||||
- name: Build and push Dockerfile-ultralite
|
- name: Build and push Dockerfile-ultra-lite
|
||||||
uses: docker/build-push-action@v4.0.0
|
uses: docker/build-push-action@v4.0.0
|
||||||
|
if: github.ref != 'refs/heads/main'
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile-ultralite
|
file: ./Dockerfile-ultra-lite
|
||||||
push: true
|
push: true
|
||||||
cache-from: type=gha
|
cache-from: type=gha
|
||||||
cache-to: type=gha,mode=max
|
cache-to: type=gha,mode=max
|
||||||
tags: ${{ steps.meta2.outputs.tags }}
|
tags: ${{ steps.meta2.outputs.tags }}
|
||||||
labels: ${{ steps.meta2.outputs.labels }}
|
labels: ${{ steps.meta2.outputs.labels }}
|
||||||
|
build-args:
|
||||||
|
VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
|
||||||
platforms: linux/amd64,linux/arm64/v8
|
platforms: linux/amd64,linux/arm64/v8
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
- name: Generate tags lite
|
||||||
|
id: meta3
|
||||||
|
uses: docker/metadata-action@v4.4.0
|
||||||
|
if: github.ref != 'refs/heads/main'
|
||||||
|
with:
|
||||||
|
images: |
|
||||||
|
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
|
||||||
|
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/s-pdf
|
||||||
|
tags: |
|
||||||
|
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}-lite,enable=${{ github.ref == 'refs/heads/master' }}
|
||||||
|
type=raw,value=latest-lite,enable=${{ github.ref == 'refs/heads/master' }}
|
||||||
|
|
||||||
|
|
||||||
|
- name: Build and push Dockerfile-lite
|
||||||
|
uses: docker/build-push-action@v4.0.0
|
||||||
|
if: github.ref != 'refs/heads/main'
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./Dockerfile-lite
|
||||||
|
push: true
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
tags: ${{ steps.meta3.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta3.outputs.labels }}
|
||||||
|
build-args:
|
||||||
|
VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
|
||||||
|
platforms: linux/amd64,linux/arm64/v8
|
||||||
|
- name: Build and Push Helm Chart
|
||||||
|
run: |
|
||||||
|
helm package chart/stirling-pdf
|
||||||
|
helm push stirling-pdf-chart-1.0.0.tgz oci://registry-1.docker.io/frooodle
|
||||||
|
|||||||
55
.github/workflows/releaseArtifacts.yml
vendored
Normal file
55
.github/workflows/releaseArtifacts.yml
vendored
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
name: Release Artifacts
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [created]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
push:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
enable_security: [true, false]
|
||||||
|
include:
|
||||||
|
- enable_security: true
|
||||||
|
file_suffix: '-with-login'
|
||||||
|
- enable_security: false
|
||||||
|
file_suffix: ''
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3.5.2
|
||||||
|
|
||||||
|
- name: Set up JDK 17
|
||||||
|
uses: actions/setup-java@v3.11.0
|
||||||
|
with:
|
||||||
|
java-version: '17'
|
||||||
|
distribution: 'temurin'
|
||||||
|
|
||||||
|
- name: Grant execute permission for gradlew
|
||||||
|
run: chmod +x gradlew
|
||||||
|
|
||||||
|
- name: Generate jar (With Security=${{ matrix.enable_security }})
|
||||||
|
run: ./gradlew clean createExe
|
||||||
|
env:
|
||||||
|
DOCKER_ENABLE_SECURITY: ${{ matrix.enable_security }}
|
||||||
|
|
||||||
|
- name: Upload binaries to release
|
||||||
|
uses: svenstaro/upload-release-action@v2
|
||||||
|
with:
|
||||||
|
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
file: ./build/launch4j/Stirling-PDF.exe
|
||||||
|
asset_name: Stirling-PDF${{ matrix.file_suffix }}.exe
|
||||||
|
tag: ${{ github.ref }}
|
||||||
|
overwrite: true
|
||||||
|
|
||||||
|
- name: Get version number
|
||||||
|
id: versionNumber
|
||||||
|
run: echo "::set-output name=versionNumber::$(./gradlew printVersion --quiet | tail -1)"
|
||||||
|
|
||||||
|
- name: Upload jar binaries to release
|
||||||
|
uses: svenstaro/upload-release-action@v2
|
||||||
|
with:
|
||||||
|
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
file: ./build/libs/Stirling-PDF-${{ steps.versionNumber.outputs.versionNumber }}.jar
|
||||||
|
asset_name: Stirling-PDF${{ matrix.file_suffix }}.jar
|
||||||
|
tag: ${{ github.ref }}
|
||||||
|
overwrite: true
|
||||||
37
.github/workflows/swagger.yml
vendored
Normal file
37
.github/workflows/swagger.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
name: Update Swagger
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
jobs:
|
||||||
|
push:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
|
||||||
|
- uses: actions/checkout@v3.5.2
|
||||||
|
|
||||||
|
- name: Set up JDK 17
|
||||||
|
uses: actions/setup-java@v3.11.0
|
||||||
|
with:
|
||||||
|
java-version: '17'
|
||||||
|
distribution: 'temurin'
|
||||||
|
|
||||||
|
- name: Grant execute permission for gradlew
|
||||||
|
run: chmod +x gradlew
|
||||||
|
|
||||||
|
- name: Generate Swagger documentation
|
||||||
|
run: ./gradlew generateOpenApiDocs
|
||||||
|
|
||||||
|
- name: Upload Swagger Documentation to SwaggerHub
|
||||||
|
run: ./gradlew swaggerhubUpload
|
||||||
|
env:
|
||||||
|
SWAGGERHUB_API_KEY: ${{ secrets.SWAGGERHUB_API_KEY }}
|
||||||
|
|
||||||
|
- name: Set API version as published and default on SwaggerHub
|
||||||
|
run: |
|
||||||
|
curl -X PUT -H "Authorization: ${SWAGGERHUB_API_KEY}" "https://api.swaggerhub.com/apis/Frooodle/Stirling-PDF/${{ steps.versionNumber.outputs.versionNumber }}/settings/lifecycle" -H "accept: application/json" -H "Content-Type: application/json" -d "{\"published\":true,\"default\":true}"
|
||||||
|
env:
|
||||||
|
SWAGGERHUB_API_KEY: ${{ secrets.SWAGGERHUB_API_KEY }}
|
||||||
241
.gitignore
vendored
241
.gitignore
vendored
@@ -1,114 +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
|
||||||
# Gradle
|
pipeline/watchedFolders/
|
||||||
.gradle
|
pipeline/finishedFolders/
|
||||||
.lock
|
#### Stirling-PDF Files ###
|
||||||
|
customFiles/
|
||||||
# External tool builders
|
configs/
|
||||||
.externalToolBuilders/
|
watchedFolders/
|
||||||
|
|
||||||
# Locally stored "Eclipse launch configurations"
|
|
||||||
*.launch
|
# Gradle
|
||||||
|
.gradle
|
||||||
# PyDev specific (Python IDE for Eclipse)
|
.lock
|
||||||
*.pydevproject
|
|
||||||
|
# External tool builders
|
||||||
# CDT-specific (C/C++ Development Tooling)
|
.externalToolBuilders/
|
||||||
.cproject
|
|
||||||
|
# Locally stored "Eclipse launch configurations"
|
||||||
# CDT- autotools
|
*.launch
|
||||||
.autotools
|
|
||||||
|
# PyDev specific (Python IDE for Eclipse)
|
||||||
# Java annotation processor (APT)
|
*.pydevproject
|
||||||
.factorypath
|
|
||||||
|
# CDT-specific (C/C++ Development Tooling)
|
||||||
# PDT-specific (PHP Development Tools)
|
.cproject
|
||||||
.buildpath
|
|
||||||
|
# CDT- autotools
|
||||||
# sbteclipse plugin
|
.autotools
|
||||||
.target
|
|
||||||
|
# Java annotation processor (APT)
|
||||||
# Tern plugin
|
.factorypath
|
||||||
.tern-project
|
|
||||||
|
# PDT-specific (PHP Development Tools)
|
||||||
# TeXlipse plugin
|
.buildpath
|
||||||
.texlipse
|
|
||||||
|
# sbteclipse plugin
|
||||||
# STS (Spring Tool Suite)
|
.target
|
||||||
.springBeans
|
|
||||||
|
# Tern plugin
|
||||||
# Code Recommenders
|
.tern-project
|
||||||
.recommenders/
|
|
||||||
|
# TeXlipse plugin
|
||||||
# Annotation Processing
|
.texlipse
|
||||||
.apt_generated/
|
|
||||||
.apt_generated_test/
|
# STS (Spring Tool Suite)
|
||||||
|
.springBeans
|
||||||
# Scala IDE specific (Scala & Java development for Eclipse)
|
|
||||||
.cache-main
|
# Code Recommenders
|
||||||
.scala_dependencies
|
.recommenders/
|
||||||
.worksheet
|
|
||||||
|
# Annotation Processing
|
||||||
# Uncomment this line if you wish to ignore the project description file.
|
.apt_generated/
|
||||||
# Typically, this file would be tracked if it contains build/dependency configurations:
|
.apt_generated_test/
|
||||||
#.project
|
|
||||||
|
# Scala IDE specific (Scala & Java development for Eclipse)
|
||||||
### Eclipse Patch ###
|
.cache-main
|
||||||
# Spring Boot Tooling
|
.scala_dependencies
|
||||||
.sts4-cache/
|
.worksheet
|
||||||
|
|
||||||
### Git ###
|
# Uncomment this line if you wish to ignore the project description file.
|
||||||
# Created by git for backups. To disable backups in Git:
|
# Typically, this file would be tracked if it contains build/dependency configurations:
|
||||||
# $ git config --global mergetool.keepBackup false
|
#.project
|
||||||
*.orig
|
|
||||||
|
### Eclipse Patch ###
|
||||||
# Created by git when using merge tools for conflicts
|
# Spring Boot Tooling
|
||||||
*.BACKUP.*
|
.sts4-cache/
|
||||||
*.BASE.*
|
|
||||||
*.LOCAL.*
|
### Git ###
|
||||||
*.REMOTE.*
|
# Created by git for backups. To disable backups in Git:
|
||||||
*_BACKUP_*.txt
|
# $ git config --global mergetool.keepBackup false
|
||||||
*_BASE_*.txt
|
*.orig
|
||||||
*_LOCAL_*.txt
|
|
||||||
*_REMOTE_*.txt
|
# Created by git when using merge tools for conflicts
|
||||||
|
*.BACKUP.*
|
||||||
### Java ###
|
*.BASE.*
|
||||||
# Compiled class file
|
*.LOCAL.*
|
||||||
*.class
|
*.REMOTE.*
|
||||||
|
*_BACKUP_*.txt
|
||||||
# Log file
|
*_BASE_*.txt
|
||||||
*.log
|
*_LOCAL_*.txt
|
||||||
|
*_REMOTE_*.txt
|
||||||
# BlueJ files
|
|
||||||
*.ctxt
|
### Java ###
|
||||||
|
# Compiled class file
|
||||||
# Mobile Tools for Java (J2ME)
|
*.class
|
||||||
.mtj.tmp/
|
|
||||||
|
# Log file
|
||||||
# Package Files #
|
*.log
|
||||||
*.jar
|
|
||||||
*.war
|
# BlueJ files
|
||||||
*.nar
|
*.ctxt
|
||||||
*.ear
|
|
||||||
*.zip
|
# Mobile Tools for Java (J2ME)
|
||||||
*.tar.gz
|
.mtj.tmp/
|
||||||
*.rar
|
|
||||||
|
# Package Files #
|
||||||
/build
|
*.jar
|
||||||
|
*.war
|
||||||
/.vscode
|
*.nar
|
||||||
|
*.ear
|
||||||
|
*.zip
|
||||||
|
*.tar.gz
|
||||||
|
*.rar
|
||||||
|
*.db
|
||||||
|
/build
|
||||||
|
|
||||||
|
/.vscode
|
||||||
|
/.idea
|
||||||
|
|
||||||
|
# Ignore Mac DS_Store files
|
||||||
|
.DS_Store
|
||||||
|
**/.DS_Store
|
||||||
55
Dockerfile
55
Dockerfile
@@ -1,22 +1,47 @@
|
|||||||
# Build jbig2enc in a separate stage
|
# Use the base image
|
||||||
FROM frooodle/stirling-pdf-base:beta3
|
FROM frooodle/stirling-pdf-base:version8
|
||||||
|
|
||||||
# Create scripts folder and copy local scripts
|
ARG VERSION_TAG
|
||||||
RUN mkdir /scripts
|
|
||||||
|
# Set Environment Variables
|
||||||
|
ENV DOCKER_ENABLE_SECURITY=false \
|
||||||
|
HOME=/home/stirlingpdfuser \
|
||||||
|
VERSION_TAG=$VERSION_TAG \
|
||||||
|
JAVA_TOOL_OPTIONS="$JAVA_TOOL_OPTIONS -XX:MaxRAMPercentage=75"
|
||||||
|
# PUID=1000 \
|
||||||
|
# PGID=1000 \
|
||||||
|
# UMASK=022 \
|
||||||
|
|
||||||
|
|
||||||
|
# Create user and group
|
||||||
|
##RUN groupadd -g $PGID stirlingpdfgroup && \
|
||||||
|
## useradd -u $PUID -g stirlingpdfgroup -s /bin/sh stirlingpdfuser && \
|
||||||
|
## mkdir -p $HOME && chown stirlingpdfuser:stirlingpdfgroup $HOME
|
||||||
|
|
||||||
|
# Set up necessary directories and permissions
|
||||||
|
RUN mkdir -p /scripts /usr/share/fonts/opentype/noto /usr/share/tesseract-ocr /configs /logs /customFiles /pipeline /pipeline/defaultWebUIConfigs /pipeline/watchedFolders /pipeline/finishedFolders
|
||||||
|
##&& \
|
||||||
|
## chown -R stirlingpdfuser:stirlingpdfgroup /scripts /usr/share/fonts/opentype/noto /usr/share/tesseract-ocr /configs /customFiles && \
|
||||||
|
## chown -R stirlingpdfuser:stirlingpdfgroup /usr/share/tesseract-ocr-original
|
||||||
|
|
||||||
|
# Copy necessary files
|
||||||
COPY ./scripts/* /scripts/
|
COPY ./scripts/* /scripts/
|
||||||
|
COPY ./pipeline/ /pipeline/
|
||||||
# Copy the application JAR file
|
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
|
||||||
|
|
||||||
# Expose the application port
|
# Set font cache and permissions
|
||||||
|
RUN fc-cache -f -v && chmod +x /scripts/*
|
||||||
|
|
||||||
|
##&& \
|
||||||
|
## chown stirlingpdfuser:stirlingpdfgroup /app.jar && \
|
||||||
|
## chmod +x /scripts/init.sh
|
||||||
|
|
||||||
|
# Expose necessary ports
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|
||||||
# Set environment variables
|
# Set user and run command
|
||||||
ENV APP_HOME_NAME="Stirling PDF"
|
##USER stirlingpdfuser
|
||||||
#ENV APP_HOME_DESCRIPTION="Personal PDF Website!"
|
|
||||||
#ENV APP_NAVBAR_NAME="Stirling PDF"
|
|
||||||
|
|
||||||
# Run the application
|
|
||||||
RUN chmod +x /scripts/init.sh
|
|
||||||
ENTRYPOINT ["/scripts/init.sh"]
|
ENTRYPOINT ["/scripts/init.sh"]
|
||||||
CMD ["java", "-jar", "/app.jar"]
|
CMD ["java", "-Dfile.encoding=UTF-8", "-jar", "/app.jar"]
|
||||||
|
|||||||
65
Dockerfile-lite
Normal file
65
Dockerfile-lite
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# Build jbig2enc in a separate stage
|
||||||
|
FROM bellsoft/liberica-openjdk-debian:17
|
||||||
|
|
||||||
|
ARG VERSION_TAG
|
||||||
|
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y --no-install-recommends \
|
||||||
|
libreoffice-core \
|
||||||
|
libreoffice-common \
|
||||||
|
libreoffice-writer \
|
||||||
|
libreoffice-calc \
|
||||||
|
libreoffice-impress \
|
||||||
|
unoconv && \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
|
||||||
|
# Set Environment Variables
|
||||||
|
ENV DOCKER_ENABLE_SECURITY=false \
|
||||||
|
HOME=/home/stirlingpdfuser \
|
||||||
|
VERSION_TAG=$VERSION_TAG \
|
||||||
|
JAVA_TOOL_OPTIONS="$JAVA_TOOL_OPTIONS -XX:MaxRAMPercentage=75"
|
||||||
|
# PUID=1000 \
|
||||||
|
# PGID=1000 \
|
||||||
|
# UMASK=022 \
|
||||||
|
|
||||||
|
# Create user and group
|
||||||
|
#RUN groupadd -g $PGID stirlingpdfgroup && \
|
||||||
|
# useradd -u $PUID -g stirlingpdfgroup -s /bin/sh stirlingpdfuser && \
|
||||||
|
# mkdir -p $HOME && chown stirlingpdfuser:stirlingpdfgroup $HOME
|
||||||
|
|
||||||
|
# Set up necessary directories and permissions
|
||||||
|
RUN mkdir -p /scripts /usr/share/fonts/opentype/noto /configs /customFiles /logs /pipeline /pipeline/defaultWebUIConfigs /pipeline/watchedFolders /pipeline/finishedFolders
|
||||||
|
|
||||||
|
# chown -R stirlingpdfuser:stirlingpdfgroup /usr/share/fonts/opentype/noto /configs /customFiles
|
||||||
|
|
||||||
|
# Copy necessary files
|
||||||
|
COPY ./scripts/download-security-jar.sh /scripts/download-security-jar.sh
|
||||||
|
COPY ./scripts/init-without-ocr.sh /scripts/init-without-ocr.sh
|
||||||
|
COPY ./pipeline/ /pipeline/
|
||||||
|
COPY src/main/resources/static/fonts/*.ttf /usr/share/fonts/opentype/noto/
|
||||||
|
COPY src/main/resources/static/fonts/*.otf /usr/share/fonts/opentype/noto/
|
||||||
|
COPY build/libs/*.jar app.jar
|
||||||
|
|
||||||
|
# Set font cache and permissions
|
||||||
|
RUN fc-cache -f -v && \
|
||||||
|
chmod +x /scripts/init-without-ocr.sh && \
|
||||||
|
chmod +x /scripts/download-security-jar.sh
|
||||||
|
|
||||||
|
|
||||||
|
# chown stirlingpdfuser:stirlingpdfgroup /app.jar
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Expose the application port
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
# Set environment variables
|
||||||
|
ENV ENDPOINTS_GROUPS_TO_REMOVE=Python,OpenCV,OCRmyPDF
|
||||||
|
ENV DOCKER_ENABLE_SECURITY=false
|
||||||
|
|
||||||
|
# Run the application
|
||||||
|
#USER stirlingpdfuser
|
||||||
|
ENTRYPOINT ["/scripts/init-without-ocr.sh"]
|
||||||
|
CMD ["java", "-Dfile.encoding=UTF-8", "-jar", "/app.jar"]
|
||||||
46
Dockerfile-ultra-lite
Normal file
46
Dockerfile-ultra-lite
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# Build jbig2enc in a separate stage
|
||||||
|
FROM bellsoft/liberica-openjdk-alpine:17
|
||||||
|
|
||||||
|
ARG VERSION_TAG
|
||||||
|
|
||||||
|
# Set Environment Variables
|
||||||
|
ENV DOCKER_ENABLE_SECURITY=false \
|
||||||
|
HOME=/home/stirlingpdfuser \
|
||||||
|
VERSION_TAG=$VERSION_TAG \
|
||||||
|
JAVA_TOOL_OPTIONS="$JAVA_TOOL_OPTIONS -XX:MaxRAMPercentage=75"
|
||||||
|
# PUID=1000 \
|
||||||
|
# PGID=1000 \
|
||||||
|
# UMASK=022 \
|
||||||
|
|
||||||
|
# Create user and group using Alpine's addgroup and adduser
|
||||||
|
#RUN addgroup -g $PGID stirlingpdfgroup && \
|
||||||
|
# adduser -u $PUID -G stirlingpdfgroup -s /bin/sh -D stirlingpdfuser && \
|
||||||
|
# mkdir -p $HOME && chown stirlingpdfuser:stirlingpdfgroup $HOME
|
||||||
|
|
||||||
|
# Set up necessary directories and permissions
|
||||||
|
#RUN mkdir -p /scripts /configs /customFiles && \
|
||||||
|
# chown -R stirlingpdfuser:stirlingpdfgroup /scripts /configs /customFiles /logs /pipeline /pipeline/defaultWebUIConfigs /pipeline/watchedFolders /pipeline/finishedFolders
|
||||||
|
|
||||||
|
RUN mkdir -p /scripts /usr/share/fonts/opentype/noto /configs /customFiles
|
||||||
|
COPY ./scripts/download-security-jar.sh /scripts/download-security-jar.sh
|
||||||
|
COPY ./scripts/init-without-ocr.sh /scripts/init-without-ocr.sh
|
||||||
|
COPY ./pipeline/ /pipeline/
|
||||||
|
COPY build/libs/*.jar app.jar
|
||||||
|
|
||||||
|
# Set font cache and permissions
|
||||||
|
#RUN chown stirlingpdfuser:stirlingpdfgroup /app.jar
|
||||||
|
|
||||||
|
RUN chmod +x /scripts/init-without-ocr.sh && \
|
||||||
|
chmod +x /scripts/download-security-jar.sh && \
|
||||||
|
apk add --no-cache curl
|
||||||
|
|
||||||
|
# Expose the application port
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
# Set environment variables
|
||||||
|
ENV ENDPOINTS_GROUPS_TO_REMOVE=CLI
|
||||||
|
|
||||||
|
ENTRYPOINT ["/scripts/init-without-ocr.sh"]
|
||||||
|
|
||||||
|
# Run the application
|
||||||
|
CMD ["java", "-Dfile.encoding=UTF-8", "-jar", "/app.jar"]
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
# Build jbig2enc in a separate stage
|
|
||||||
FROM openjdk:17-jdk-slim
|
|
||||||
|
|
||||||
# Copy the application JAR file
|
|
||||||
COPY build/libs/*.jar app.jar
|
|
||||||
|
|
||||||
# Expose the application port
|
|
||||||
EXPOSE 8080
|
|
||||||
|
|
||||||
# Set environment variables
|
|
||||||
ENV GROUPS_TO_REMOVE=LibreOffice,CLI
|
|
||||||
|
|
||||||
# Run the application
|
|
||||||
CMD ["java", "-jar", "/app.jar"]
|
|
||||||
@@ -1,37 +1,50 @@
|
|||||||
# Main stage
|
# Main stage
|
||||||
FROM openjdk:17-jdk-slim AS base
|
FROM ubuntu:latest AS base
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# JDK for app
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
apt-get install -y --no-install-recommends \
|
apt-get install -y --no-install-recommends \
|
||||||
libreoffice-core-nogui \
|
openjdk-17-jre
|
||||||
|
|
||||||
|
# Doc conversion
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y --no-install-recommends \
|
||||||
|
libreoffice-core \
|
||||||
libreoffice-common \
|
libreoffice-common \
|
||||||
libreoffice-writer-nogui \
|
libreoffice-writer \
|
||||||
libreoffice-calc-nogui \
|
libreoffice-calc \
|
||||||
libreoffice-impress-nogui \
|
libreoffice-impress \
|
||||||
python3-uno \
|
python3-uno \
|
||||||
|
curl \
|
||||||
|
unoconv
|
||||||
|
|
||||||
|
|
||||||
|
# OCR MY PDF (unpaper for descew and other advanced featues)
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends software-properties-common gnupg2 && \
|
||||||
|
add-apt-repository ppa:alex-p/tesseract-ocr5 && apt install -y --no-install-recommends tesseract-ocr && \
|
||||||
|
apt-get update && \
|
||||||
|
apt-get install -y --no-install-recommends \
|
||||||
|
ghostscript \
|
||||||
python3-pip \
|
python3-pip \
|
||||||
unoconv \
|
ocrmypdf \
|
||||||
pngquant \
|
unpaper && \
|
||||||
unpaper \
|
pip install --upgrade pip && \
|
||||||
ocrmypdf && \
|
pip install --no-cache-dir --upgrade ocrmypdf && \
|
||||||
rm -rf /var/lib/apt/lists/* && \
|
pip install --no-cache-dir --upgrade pillow==10.0.1 reportlab==3.6.13 wheel==0.38.1 setuptools==65.5.1 pyjwt==2.4.0 cryptography==39.0.1
|
||||||
|
|
||||||
|
|
||||||
|
#CV and HTML
|
||||||
|
RUN pip install --no-cache-dir opencv-python-headless WeasyPrint
|
||||||
|
|
||||||
|
|
||||||
|
# cleanup and etc
|
||||||
|
RUN rm -rf /var/lib/apt/lists/* && \
|
||||||
mkdir /usr/share/tesseract-ocr-original && \
|
mkdir /usr/share/tesseract-ocr-original && \
|
||||||
cp -r /usr/share/tesseract-ocr/* /usr/share/tesseract-ocr-original && \
|
cp -r /usr/share/tesseract-ocr/* /usr/share/tesseract-ocr-original && \
|
||||||
rm -rf /usr/share/tesseract-ocr
|
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 && \
|
|
||||||
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
|
|
||||||
46
Endpoint-groups.md
Normal file
46
Endpoint-groups.md
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
| Operation | PageOps | Convert | Security | Other | CLI | Python | OpenCV | LibreOffice | OCRmyPDF | Java | Javascript |
|
||||||
|
|---------------------|---------|---------|----------|-------|------|--------|--------|-------------|----------|----------|------------|
|
||||||
|
| adjust-contrast | ✔️ | | | | | | | | | | ✔️ |
|
||||||
|
| auto-split-pdf | ✔️ | | | | | | | | | ✔️ | |
|
||||||
|
| crop | ✔️ | | | | | | | | | ✔️ | |
|
||||||
|
| extract-page | ✔️ | | | | | | | | | ✔️ | |
|
||||||
|
| merge-pdfs | ✔️ | | | | | | | | | ✔️ | |
|
||||||
|
| multi-page-layout | ✔️ | | | | | | | | | ✔️ | |
|
||||||
|
| pdf-organizer | ✔️ | | | | | | | | | ✔️ | ✔️ |
|
||||||
|
| pdf-to-single-page | ✔️ | | | | | | | | | ✔️ | |
|
||||||
|
| remove-pages | ✔️ | | | | | | | | | ✔️ | |
|
||||||
|
| rotate-pdf | ✔️ | | | | | | | | | ✔️ | |
|
||||||
|
| scale-pages | ✔️ | | | | | | | | | ✔️ | |
|
||||||
|
| split-pdfs | ✔️ | | | | | | | | | ✔️ | |
|
||||||
|
| file-to-pdf | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
||||||
|
| img-to-pdf | | ✔️ | | | | | | | | ✔️ | |
|
||||||
|
| pdf-to-html | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
||||||
|
| pdf-to-img | | ✔️ | | | | | | | | ✔️ | |
|
||||||
|
| pdf-to-pdfa | | ✔️ | | | ✔️ | | | | ✔️ | | |
|
||||||
|
| pdf-to-markdown | | ✔️ | | | | | | | | ✔️ | |
|
||||||
|
| pdf-to-presentation | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
||||||
|
| pdf-to-text | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
||||||
|
| pdf-to-word | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
||||||
|
| pdf-to-xml | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
||||||
|
| xlsx-to-pdf | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
||||||
|
| add-password | | | ✔️ | | | | | | | ✔️ | |
|
||||||
|
| add-watermark | | | ✔️ | | | | | | | ✔️ | |
|
||||||
|
| cert-sign | | | ✔️ | | | | | | | ✔️ | |
|
||||||
|
| change-permissions | | | ✔️ | | | | | | | ✔️ | |
|
||||||
|
| remove-password | | | ✔️ | | | | | | | ✔️ | |
|
||||||
|
| sanitize-pdf | | | ✔️ | | | | | | | ✔️ | |
|
||||||
|
| add-image | | | | ✔️ | | | | | | ✔️ | |
|
||||||
|
| add-page-numbers | | | | ✔️ | | | | | | ✔️ | |
|
||||||
|
| auto-rename | | | | ✔️ | | | | | | ✔️ | |
|
||||||
|
| change-metadata | | | | ✔️ | | | | | | ✔️ | |
|
||||||
|
| compare | | | | ✔️ | | | | | | | ✔️ |
|
||||||
|
| compress-pdf | | | | ✔️ | ✔️ | | | | ✔️ | | |
|
||||||
|
| extract-image-scans | | | | ✔️ | ✔️ | ✔️ | ✔️ | | | | |
|
||||||
|
| extract-images | | | | ✔️ | | | | | | ✔️ | |
|
||||||
|
| flatten | | | | ✔️ | | | | | | | ✔️ |
|
||||||
|
| get-info-on-pdf | | | | ✔️ | | | | | | ✔️ | |
|
||||||
|
| ocr-pdf | | | | ✔️ | ✔️ | | | | ✔️ | | |
|
||||||
|
| remove-blanks | | | | ✔️ | ✔️ | ✔️ | ✔️ | | | | |
|
||||||
|
| repair | | | | ✔️ | ✔️ | | | ✔️ | | | |
|
||||||
|
| show-javascript | | | | ✔️ | | | | | | | ✔️ |
|
||||||
|
| sign | | | | ✔️ | | | | | | | ✔️ |
|
||||||
@@ -8,7 +8,7 @@ Fork Stirling-PDF and make a new branch out of Main
|
|||||||
|
|
||||||
Then add reference to the language in the navbar by adding a new language entry to the dropdown
|
Then add reference to the language in the navbar by adding a new language entry to the dropdown
|
||||||
|
|
||||||
https://github.com/Frooodle/Stirling-PDF/blob/main/src/main/resources/templates/fragments/navbar.html#L306
|
https://github.com/Frooodle/Stirling-PDF/blob/main/src/main/resources/templates/fragments/languages.html
|
||||||
and add a flag svg file to
|
and add a flag svg file to
|
||||||
https://github.com/Frooodle/Stirling-PDF/tree/main/src/main/resources/static/images/flags
|
https://github.com/Frooodle/Stirling-PDF/tree/main/src/main/resources/static/images/flags
|
||||||
Any SVG flags are fine, i got most of mine from [here](https://flagicons.lipis.dev/)
|
Any SVG flags are fine, i got most of mine from [here](https://flagicons.lipis.dev/)
|
||||||
@@ -25,7 +25,7 @@ The data-language-code is the code used to reference the file in the next step.
|
|||||||
|
|
||||||
Start by copying the existing english property file
|
Start by copying the existing english property file
|
||||||
|
|
||||||
[https://github.com/Frooodle/Stirling-PDF/tree/langSetup/src/main/resources/messages_en_GB.properties](https://github.com/Frooodle/Stirling-PDF/blob/main/src/main/resources/messages_en_US.properties)
|
[https://github.com/Frooodle/Stirling-PDF/blob/main/src/main/resources/messages_en_GB.properties](https://github.com/Frooodle/Stirling-PDF/blob/main/src/main/resources/messages_en_GB.properties)
|
||||||
|
|
||||||
Copy and rename it to messages_{your data-language-code here}.properties, in the polish example you would set the name to messages_pl_PL.properties
|
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,8 +2,11 @@
|
|||||||
|
|
||||||
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 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!
|
||||||
|
|
||||||
## Language Packs
|
## Language Packs
|
||||||
@@ -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/tesseract-ocr/5/tessdata` (Debian) or `/usr/share/tesseract/tessdata` (Fedora)
|
||||||
|
|
||||||
# DO NOT REMOVE EXISTING ENG.TRAINEDDATA, ITS REQUIRED.
|
# DO NOT REMOVE EXISTING ENG.TRAINEDDATA, IT'S REQUIRED.
|
||||||
|
|
||||||
#### Docker
|
#### 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/tesseract-ocr/5/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/tesseract-ocr/5/tessdata
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Non-Docker
|
#### Non-Docker
|
||||||
|
|||||||
20
Jenkinsfile
vendored
20
Jenkinsfile
vendored
@@ -22,12 +22,24 @@ pipeline {
|
|||||||
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,14 +95,14 @@ 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
|
||||||
@@ -123,7 +125,7 @@ This folder is required for the python scripts using OpenCV
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo mkdir /opt/Stirling-PDF &&\
|
sudo mkdir /opt/Stirling-PDF &&\
|
||||||
sudo mv /build/libs/S-PDF-*.jar /opt/Stirling-PDF/ &&\
|
sudo mv ./build/libs/Stirling-PDF-*.jar /opt/Stirling-PDF/ &&\
|
||||||
sudo mv scripts /opt/Stirling-PDF/ &&\
|
sudo mv scripts /opt/Stirling-PDF/ &&\
|
||||||
echo "Scripts installed."
|
echo "Scripts installed."
|
||||||
```
|
```
|
||||||
@@ -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/tesseract-ocr/5/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.
|
||||||
|
|||||||
241
README.md
241
README.md
@@ -8,46 +8,79 @@
|
|||||||
[](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)
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
Please feel free to submit feature requests or report bugs either through GitHub issues or on our [Discord](https://discord.gg/Cn8pWhQRxZ)
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
- Full intractable GUI for merging/splitting/rotating/moving PDFs and their pages.
|
|
||||||
- Split PDFs into multiple files at specified page numbers or extract all pages as individual files.
|
|
||||||
- Merge multiple PDFs together into a single resultant file
|
|
||||||
- Convert PDFs to and from images
|
|
||||||
- Reorganize PDF pages into different orders.
|
|
||||||
- Add/Generate signatures
|
|
||||||
- Flatten PDFs
|
|
||||||
- Repair PDFs
|
|
||||||
- Detect and remove blank pages
|
|
||||||
- Compare 2 PDFs and show differences in text
|
|
||||||
- Add images to PDFs
|
|
||||||
- Rotating PDFs in 90 degree increments.
|
|
||||||
- Compressing PDFs to decrease their filesize. (Using OCRMyPDF)
|
|
||||||
- Add and remove passwords
|
|
||||||
- Set PDF Permissions
|
|
||||||
- Add watermark(s)
|
|
||||||
- Convert Any common file to PDF (using LibreOffice)
|
|
||||||
- Convert PDF to Word/Powerpoint/Others (using LibreOffice)
|
|
||||||
- Extract images from PDF
|
|
||||||
- OCR on PDF (Using OCRMyPDF)
|
|
||||||
- Edit metadata
|
|
||||||
- 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/Frooodle/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)
|
||||||
|
|
||||||
For a overview of the tasks and the technology each uses please view [groups.md](https://github.com/Frooodle/Stirling-PDF/blob/main/Groups.md)
|
|
||||||
|
## **PDF Features**
|
||||||
|
|
||||||
|
### **Page Operations**
|
||||||
|
- View and modify PDFs - View multi page PDFs with custom viewing sorting and searching. Plus on page edit features like annotate, draw and adding text and images. (Using PDF.js with Joxit and Liberation.Liberation fonts)
|
||||||
|
- Full interactive GUI for merging/splitting/rotating/moving PDFs and their pages.
|
||||||
|
- Merge multiple PDFs together into a single resultant file.
|
||||||
|
- Split PDFs into multiple files at specified page numbers or extract all pages as individual files.
|
||||||
|
- Reorganize PDF pages into different orders.
|
||||||
|
- Rotate PDFs in 90-degree increments.
|
||||||
|
- Remove pages.
|
||||||
|
- Multi-page layout (Format PDFs into a multi-paged page).
|
||||||
|
- Scale page contents size by set %.
|
||||||
|
- Adjust Contrast.
|
||||||
|
- Crop PDF.
|
||||||
|
- Auto Split PDF (With physically scanned page dividers).
|
||||||
|
- Extract page(s).
|
||||||
|
- Convert PDF to a single page.
|
||||||
|
|
||||||
|
### **Conversion Operations**
|
||||||
|
- Convert PDFs to and from images.
|
||||||
|
- Convert any common file to PDF (using LibreOffice).
|
||||||
|
- Convert PDF to Word/Powerpoint/Others (using LibreOffice).
|
||||||
|
- Convert HTML to PDF.
|
||||||
|
- URL to PDF.
|
||||||
|
- Markdown to PDF.
|
||||||
|
|
||||||
|
### **Security & Permissions**
|
||||||
|
- Add and remove passwords.
|
||||||
|
- Change/set PDF Permissions.
|
||||||
|
- Add watermark(s).
|
||||||
|
- Certify/sign PDFs.
|
||||||
|
- Sanitize PDFs.
|
||||||
|
- Auto-redact text.
|
||||||
|
|
||||||
|
### **Other Operations**
|
||||||
|
- Add/Generate/Write signatures.
|
||||||
|
- Repair PDFs.
|
||||||
|
- Detect and remove blank pages.
|
||||||
|
- Compare 2 PDFs and show differences in text.
|
||||||
|
- Add images to PDFs.
|
||||||
|
- Compress PDFs to decrease their filesize (Using OCRMyPDF).
|
||||||
|
- Extract images from PDF.
|
||||||
|
- Extract images from Scans.
|
||||||
|
- Add page numbers.
|
||||||
|
- Auto rename file by detecting PDF header text.
|
||||||
|
- OCR on PDF (Using OCRMyPDF).
|
||||||
|
- PDF/A conversion (Using OCRMyPDF).
|
||||||
|
- Edit metadata.
|
||||||
|
- Flatten PDFs.
|
||||||
|
- Get all information on a PDF to view or export as JSON.
|
||||||
|
|
||||||
|
|
||||||
|
For a overview of the tasks and the technology each uses please view [Endpoint-groups.md](https://github.com/Frooodle/Stirling-PDF/blob/main/Endpoint-groups.md)
|
||||||
Hosted instance/demo of the app can be seen [here](https://pdf.adminforge.de/) hosted by the team at adminforge.de
|
Hosted instance/demo of the app can be seen [here](https://pdf.adminforge.de/) hosted by the team at adminforge.de
|
||||||
|
|
||||||
## Technologies used
|
## Technologies used
|
||||||
@@ -65,54 +98,58 @@ Hosted instance/demo of the app can be seen [here](https://pdf.adminforge.de/) h
|
|||||||
### Locally
|
### Locally
|
||||||
Please view https://github.com/Frooodle/Stirling-PDF/blob/main/LocalRunGuide.md
|
Please view https://github.com/Frooodle/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.
|
||||||
|
To see what the different versions offer please look at our [version mapping](https://github.com/Frooodle/Stirling-PDF/blob/main/Version-groups.md)
|
||||||
|
For people that don't mind about space optimization just use the latest tag.
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
Docker Run
|
Docker Run
|
||||||
```
|
```
|
||||||
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/tesseract-ocr/5/tessdata \
|
||||||
|
-v /location/of/extraConfigs:/configs \
|
||||||
|
-v /location/of/logs:/logs \
|
||||||
|
-e DOCKER_ENABLE_SECURITY=false \
|
||||||
--name stirling-pdf \
|
--name stirling-pdf \
|
||||||
frooodle/s-pdf
|
frooodle/s-pdf:latest
|
||||||
|
|
||||||
|
|
||||||
Can also add these for customisation but are not required
|
Can also add these for customisation but are not required
|
||||||
-e APP_HOME_NAME="Stirling PDF" \
|
|
||||||
-e APP_HOME_DESCRIPTION="Your locally hosted one-stop-shop for all your PDF needs." \
|
-v /location/of/customFiles:/customFiles \
|
||||||
-e APP_NAVBAR_NAME="Stirling PDF" \
|
|
||||||
-e ALLOW_GOOGLE_VISIBILITY="true" \
|
|
||||||
-e APP_ROOT_PATH="/" \
|
|
||||||
-e APP_LOCALE="en_GB" \
|
|
||||||
```
|
```
|
||||||
Docker Compose
|
Docker Compose
|
||||||
```
|
```
|
||||||
version: '3.3'
|
version: '3.3'
|
||||||
services:
|
services:
|
||||||
stirling-pdf:
|
stirling-pdf:
|
||||||
image: frooodle/s-pdf
|
image: frooodle/s-pdf:latest
|
||||||
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/tesseract-ocr/5/tessdata #Required for extra OCR languages
|
||||||
# - /location/of/extraConfigs:/configs
|
- /location/of/extraConfigs:/configs
|
||||||
# environment:
|
# - /location/of/customFiles:/customFiles/
|
||||||
# APP_LOCALE: en_GB
|
# - /location/of/logs:/logs/
|
||||||
# APP_HOME_NAME: Stirling PDF
|
environment:
|
||||||
# APP_HOME_DESCRIPTION: Your locally hosted one-stop-shop for all your PDF needs.
|
- DOCKER_ENABLE_SECURITY=false
|
||||||
# APP_NAVBAR_NAME: Stirling PDF
|
|
||||||
# APP_ROOT_PATH: /
|
|
||||||
# ALLOW_GOOGLE_VISIBILITY: true
|
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
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/Frooodle/Stirling-PDF/blob/main/HowToUseOCR.md
|
||||||
|
|
||||||
## Want to add your own language?
|
## Want to add your own language?
|
||||||
Stirling PDF currently supports
|
Stirling PDF currently supports 21!
|
||||||
- English (English) (en_GB)
|
- English (English) (en_GB)
|
||||||
|
- 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)
|
||||||
@@ -124,52 +161,114 @@ Stirling PDF currently supports
|
|||||||
- Polish (Polski) (pl_PL)
|
- Polish (Polski) (pl_PL)
|
||||||
- Romanian (Română) (ro_RO)
|
- Romanian (Română) (ro_RO)
|
||||||
- Korean (한국어) (ko_KR)
|
- Korean (한국어) (ko_KR)
|
||||||
|
- Portuguese Brazilian (Português) (pt_BR)
|
||||||
|
- Russian (Русский) (ru_RU)
|
||||||
|
- Basque (Euskara) (eu_ES)
|
||||||
|
- Japanese (日本語) (ja_JP)
|
||||||
|
- Dutch (Nederlands) (nl_NL)
|
||||||
|
- Greek (el_GR)
|
||||||
|
- Turkish (Türkçe) (tr_TR)
|
||||||
|
- Indonesia (Bahasa Indonesia) (id_ID)
|
||||||
|
- Hindi (हिंदी) (hi_IN)
|
||||||
|
|
||||||
If you want to add your own language to Stirling-PDF please refer
|
If you want to add your own language to Stirling-PDF please refer
|
||||||
https://github.com/Frooodle/Stirling-PDF/blob/main/HowToAddNewLanguage.md
|
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!
|
And please create a PR to merge it back in so others can use it!
|
||||||
|
|
||||||
Also please note as i add new features i will google translate existing languages so that they dont lose support. This could mean that new features need grammer corrections as added.
|
|
||||||
|
|
||||||
## How to View
|
## How to View
|
||||||
1. Open a web browser and navigate to `http://localhost:8080/`
|
1. Open a web browser and navigate to `http://localhost:8080/`
|
||||||
2. Use the application by following the instructions on the website.
|
2. Use the application by following the instructions on the website.
|
||||||
|
|
||||||
|
|
||||||
## Customize App
|
## Customisation
|
||||||
Stirling PDF allows easy customization of the visible application name.
|
Stirling PDF allows easy customization of the app.
|
||||||
Simply use environment variables APP_HOME_NAME, APP_HOME_DESCRIPTION and APP_NAVBAR_NAME with Docker or Java.
|
Includes things like
|
||||||
If running Java directly, you can also pass these as properties using -D arguments.
|
- Custom application name
|
||||||
|
- Custom slogans, icons, images, and even custom HTML (via file overrides)
|
||||||
|
|
||||||
Using the same method you can also change
|
|
||||||
|
|
||||||
- The default language by providing APP_LOCALE with values like de-DE fr-FR or ar-AR (Note the - character not _ ) to select your default language (Will always default to English on invalid locale) Current accepted locales can be seen above in the Want to add your own language section
|
There are two options for this, either using the generated settings file ``settings.yml``
|
||||||
- Enable/Disable search engine visiblility with ALLOW_GOOGLE_VISIBILITY with true / false values. Default disable visiblility.
|
This file is located in the ``/configs`` directory and follows standard YAML formatting
|
||||||
- Change root URI for Stirling-PDF ie change server.com/ to server.com/pdf-app by running APP_ROOT_PATH as pdf-app
|
|
||||||
- Disable and remove endpoints and functionality from Stirling-PDF. Currently the endpoints ENDPOINTS_TO_REMOVE and GROUPS_TO_REMOVE can include comma seperated lists of endpoints and groups to disable as example ENDPOINTS_TO_REMOVE=img-to-pdf,remove-pages would disable both image to pdf and remove pages, GROUPS_TO_REMOVE=LibreOffice Would disable all things that use LibreOffice. You can see a list of all endpoints and groups [here](https://github.com/Frooodle/Stirling-PDF/blob/main/groups.md)
|
|
||||||
|
|
||||||
|
Environment variables are also supported and would override the settings file
|
||||||
|
For example in the settings.yml you have
|
||||||
|
```
|
||||||
|
system:
|
||||||
|
defaultLocale: 'en-US'
|
||||||
|
```
|
||||||
|
|
||||||
|
To have this via an environment variable you would have ``SYSTEM_DEFAULTLOCALE``
|
||||||
|
|
||||||
|
The Current list of settings is
|
||||||
|
```
|
||||||
|
security:
|
||||||
|
enableLogin: false # set to 'true' to enable login
|
||||||
|
csrfDisabled: true
|
||||||
|
|
||||||
|
system:
|
||||||
|
defaultLocale: 'en-US' # Set the default language (e.g. 'de-DE', 'fr-FR', etc)
|
||||||
|
googlevisibility: false # 'true' to allow Google visibility (via robots.txt), 'false' to disallow
|
||||||
|
customStaticFilePath: '/customFiles/static/' # Directory path for custom static files
|
||||||
|
|
||||||
|
#ui:
|
||||||
|
# appName: exampleAppName # Application's visible name
|
||||||
|
# homeDescription: I am a description # Short description or tagline shown on homepage.
|
||||||
|
# appNameNavbar: navbarName # Name displayed on the navigation bar
|
||||||
|
|
||||||
|
endpoints:
|
||||||
|
toRemove: [] # List endpoints to disable (e.g. ['img-to-pdf', 'remove-pages'])
|
||||||
|
groupsToRemove: [] # List groups to disable (e.g. ['LibreOffice'])
|
||||||
|
|
||||||
|
metrics:
|
||||||
|
enabled: true # 'true' to enable Info APIs endpoints (view http://localhost:8080/swagger-ui/index.html#/API to learn more), 'false' to disable
|
||||||
|
```
|
||||||
|
### Extra notes
|
||||||
|
- Endpoints. Currently, the endpoints ENDPOINTS_TO_REMOVE and GROUPS_TO_REMOVE can include comma separate lists of endpoints and groups to disable as example ENDPOINTS_TO_REMOVE=img-to-pdf,remove-pages would disable both image-to-pdf and remove pages, GROUPS_TO_REMOVE=LibreOffice Would disable all things that use LibreOffice. You can see a list of all endpoints and groups [here](https://github.com/Frooodle/Stirling-PDF/blob/main/Endpoint-groups.md)
|
||||||
|
- customStaticFilePath. Customise static files such as the app logo by placing files in the /customFiles/static/ directory. An example of customising app logo is placing a /customFiles/static/favicon.svg to override current SVG. This can be used to change any images/icons/css/fonts/js etc in Stirling-PDF
|
||||||
|
|
||||||
|
### Environment only parameters
|
||||||
|
- ``SYSTEM_ROOTURIPATH`` ie set to ``/pdf-app`` to Set the application's root URI to ``localhost:8080/pdf-app``
|
||||||
|
- ``SYSTEM_CONNECTIONTIMEOUTMINUTES`` to set custom connection timeout values
|
||||||
|
- ``DOCKER_ENABLE_SECURITY`` to tell docker to download security jar (required as true for auth login)
|
||||||
|
|
||||||
## API
|
## 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
|
[here](https://app.swaggerhub.com/apis-docs/Frooodle/Stirling-PDF/) or navigate to /swagger-ui/index.html of your stirling-pdf instance for your versions documentation (Or by following the API button in your settings of Stirling-PDF)
|
||||||
|
|
||||||
|
|
||||||
|
## Login authentication
|
||||||
|

|
||||||
|
### Prerequisites:
|
||||||
|
- User must have the folder ./configs volumed within docker so that it is retained during updates.
|
||||||
|
- Docker uses must download the security jar version by setting ``DOCKER_ENABLE_SECURITY`` to ``true`` in environment variables.
|
||||||
|
- Then either enable login via the settings.yml file or via setting ``SECURITY_ENABLE_LOGIN`` to ``true``
|
||||||
|
- Now the initial user will be generated with username ``admin`` and password ``stirling``. On login you will be forced to change the password to a new one. You can also use the environment variables ``SECURITY_INITIALLOGIN_USERNAME`` and ``SECURITY_INITIALLOGIN_PASSWORD`` to set your own straight away (Recommended to remove them after user creation).
|
||||||
|
|
||||||
|
Once the above has been done, on restart, a new stirling-pdf-DB.mv.db will show if everything worked.
|
||||||
|
|
||||||
|
When you login to Stirling PDF you will be redirected to /login page to login with those default credentials. After login everything should function as normal
|
||||||
|
|
||||||
|
To access your account settings go to Account settings in the settings cog menu (top right in navbar) This Account settings menu is also where you find your API key.
|
||||||
|
|
||||||
|
To add new users go to the bottom of Account settings and hit 'Admin Settings', here you can add new users. The different roles mentioned within this are for rate limiting. This is a Work in progress which will be expanding on more in future
|
||||||
|
|
||||||
|
For API usage you must provide a header with 'X-API-Key' and the associated API key for that user.
|
||||||
|
|
||||||
|
|
||||||
## FAQ
|
## FAQ
|
||||||
|
|
||||||
### Q1: Can you add authentication in Stirling PDF?
|
### Q1: What are your planned features?
|
||||||
There is no Auth within Stirling PDF and there is none planned. This feature will not be added. Instead we recommended you use trusted and secure authentication software like Authentik or Authelia.
|
|
||||||
|
|
||||||
### Q2: What are your planned features?
|
|
||||||
- Crop
|
|
||||||
- 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 sections of pages
|
- Redact text (Via UI not just automated way)
|
||||||
- Add page numbers
|
- Add Forms
|
||||||
- Auto rename (Renames file based on file title text)
|
- Multi page layout (Stich PDF pages together) support x rows y columns and custom page sizing
|
||||||
- URL to PDF
|
- Fill forms mannual and automatic
|
||||||
- Change contrast
|
|
||||||
|
|
||||||
### Q3: 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;``
|
||||||
|
|||||||
64
Version-groups.md
Normal file
64
Version-groups.md
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
|Technology | Ultra-Lite | Lite | Full |
|
||||||
|
|----------------|:----------:|:----:|:----:|
|
||||||
|
| Java | ✔️ | ✔️ | ✔️ |
|
||||||
|
| JavaScript | ✔️ | ✔️ | ✔️ |
|
||||||
|
| Libre | | ✔️ | ✔️ |
|
||||||
|
| Python | | | ✔️ |
|
||||||
|
| OpenCV | | | ✔️ |
|
||||||
|
| OCRmyPDF | | | ✔️ |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Operation | Ultra-Lite | Lite | Full
|
||||||
|
--------------------|------------|------|-----
|
||||||
|
add-page-numbers | ✔️ | ✔️ | ✔️
|
||||||
|
add-password | ✔️ | ✔️ | ✔️
|
||||||
|
add-image | ✔️ | ✔️ | ✔️
|
||||||
|
add-watermark | ✔️ | ✔️ | ✔️
|
||||||
|
adjust-contrast | ✔️ | ✔️ | ✔️
|
||||||
|
auto-split-pdf | ✔️ | ✔️ | ✔️
|
||||||
|
auto-redact | ✔️ | ✔️ | ✔️
|
||||||
|
auto-rename | ✔️ | ✔️ | ✔️
|
||||||
|
cert-sign | ✔️ | ✔️ | ✔️
|
||||||
|
crop | ✔️ | ✔️ | ✔️
|
||||||
|
change-metadata | ✔️ | ✔️ | ✔️
|
||||||
|
change-permissions | ✔️ | ✔️ | ✔️
|
||||||
|
compare | ✔️ | ✔️ | ✔️
|
||||||
|
extract-page | ✔️ | ✔️ | ✔️
|
||||||
|
extract-images | ✔️ | ✔️ | ✔️
|
||||||
|
flatten | ✔️ | ✔️ | ✔️
|
||||||
|
get-info-on-pdf | ✔️ | ✔️ | ✔️
|
||||||
|
img-to-pdf | ✔️ | ✔️ | ✔️
|
||||||
|
markdown-to-pdf | ✔️ | ✔️ | ✔️
|
||||||
|
merge-pdfs | ✔️ | ✔️ | ✔️
|
||||||
|
multi-page-layout | ✔️ | ✔️ | ✔️
|
||||||
|
overlay-pdf | ✔️ | ✔️ | ✔️
|
||||||
|
pdf-organizer | ✔️ | ✔️ | ✔️
|
||||||
|
pdf-to-csv | ✔️ | ✔️ | ✔️
|
||||||
|
pdf-to-img | ✔️ | ✔️ | ✔️
|
||||||
|
pdf-to-single-page | ✔️ | ✔️ | ✔️
|
||||||
|
remove-pages | ✔️ | ✔️ | ✔️
|
||||||
|
remove-password | ✔️ | ✔️ | ✔️
|
||||||
|
rotate-pdf | ✔️ | ✔️ | ✔️
|
||||||
|
sanitize-pdf | ✔️ | ✔️ | ✔️
|
||||||
|
scale-pages | ✔️ | ✔️ | ✔️
|
||||||
|
sign | ✔️ | ✔️ | ✔️
|
||||||
|
show-javascript | ✔️ | ✔️ | ✔️
|
||||||
|
split-by-size-or-count | ✔️ | ✔️ | ✔️
|
||||||
|
split-pdf-by-sections | ✔️ | ✔️ | ✔️
|
||||||
|
split-pdfs | ✔️ | ✔️ | ✔️
|
||||||
|
file-to-pdf | | ✔️ | ✔️
|
||||||
|
pdf-to-html | | ✔️ | ✔️
|
||||||
|
pdf-to-presentation | | ✔️ | ✔️
|
||||||
|
pdf-to-text | | ✔️ | ✔️
|
||||||
|
pdf-to-word | | ✔️ | ✔️
|
||||||
|
pdf-to-xml | | ✔️ | ✔️
|
||||||
|
repair | | ✔️ | ✔️
|
||||||
|
xlsx-to-pdf | | ✔️ | ✔️
|
||||||
|
compress-pdf | | | ✔️
|
||||||
|
extract-image-scans | | | ✔️
|
||||||
|
ocr-pdf | | | ✔️
|
||||||
|
pdf-to-pdfa | | | ✔️
|
||||||
|
remove-blanks | | | ✔️
|
||||||
182
build.gradle
182
build.gradle
@@ -1,50 +1,188 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id 'java'
|
id 'java'
|
||||||
id 'org.springframework.boot' version '3.0.6'
|
id 'org.springframework.boot' version '3.1.2'
|
||||||
id 'io.spring.dependency-management' version '1.1.0'
|
id 'io.spring.dependency-management' version '1.1.3'
|
||||||
|
id 'org.springdoc.openapi-gradle-plugin' version '1.8.0'
|
||||||
|
id "io.swagger.swaggerhub" version "1.3.2"
|
||||||
|
id 'edu.sc.seis.launch4j' version '3.0.5'
|
||||||
|
id 'com.diffplug.spotless' version '6.23.3'
|
||||||
}
|
}
|
||||||
|
|
||||||
group = 'stirling.software'
|
group = 'stirling.software'
|
||||||
version = '0.9.1'
|
version = '0.18.1'
|
||||||
sourceCompatibility = '17'
|
sourceCompatibility = '17'
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
main {
|
||||||
|
java {
|
||||||
|
if (System.getenv('DOCKER_ENABLE_SECURITY') == 'false') {
|
||||||
|
exclude 'stirling/software/SPDF/config/security/**'
|
||||||
|
exclude 'stirling/software/SPDF/controller/api/UserController.java'
|
||||||
|
exclude 'stirling/software/SPDF/controller/web/AccountWebController.java'
|
||||||
|
exclude 'stirling/software/SPDF/model/ApiKeyAuthenticationToken.java'
|
||||||
|
exclude 'stirling/software/SPDF/model/Authority.java'
|
||||||
|
exclude 'stirling/software/SPDF/model/PersistentLogin.java'
|
||||||
|
exclude 'stirling/software/SPDF/model/User.java'
|
||||||
|
exclude 'stirling/software/SPDF/repository/**'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
openApi {
|
||||||
|
apiDocsUrl = "http://localhost:8080/v1/api-docs"
|
||||||
|
outputDir = file("$projectDir")
|
||||||
|
outputFileName = "SwaggerDoc.json"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
launch4j {
|
||||||
|
icon = "${projectDir}/src/main/resources/static/favicon.ico"
|
||||||
|
|
||||||
|
outfile="Stirling-PDF.exe"
|
||||||
|
headerType="console"
|
||||||
|
jarTask = tasks.bootJar
|
||||||
|
|
||||||
|
errTitle="Encountered error, Do you have Java 17?"
|
||||||
|
downloadUrl="https://download.oracle.com/java/17/latest/jdk-17_windows-x64_bin.exe"
|
||||||
|
variables=["BROWSER_OPEN=true", "ENDPOINTS_GROUPS_TO_REMOVE=CLI"]
|
||||||
|
jreMinVersion="17"
|
||||||
|
|
||||||
|
mutexName="Stirling-PDF"
|
||||||
|
windowTitle="Stirling-PDF"
|
||||||
|
|
||||||
|
messagesStartupError="An error occurred while starting Stirling-PDF"
|
||||||
|
//messagesJreNotFoundError="This application requires a Java Runtime Environment, Please download Java 17."
|
||||||
|
messagesJreVersionError="You are running the wrong version of Java, Please download Java 17."
|
||||||
|
messagesLauncherError="Java is corrupted. Please uninstall and then install Java 17."
|
||||||
|
messagesInstanceAlreadyExists="Stirling-PDF is already running."
|
||||||
|
}
|
||||||
|
|
||||||
|
spotless {
|
||||||
|
java {
|
||||||
|
target project.fileTree('src/main/java')
|
||||||
|
|
||||||
|
googleJavaFormat('1.19.1').aosp().reorderImports(false)
|
||||||
|
|
||||||
|
importOrder('java', 'javax', 'org', 'com', 'net', 'io')
|
||||||
|
toggleOffOn()
|
||||||
|
trimTrailingWhitespace()
|
||||||
|
indentWithSpaces()
|
||||||
|
endWithNewline()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-web:3.0.6'
|
//security updates
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf:3.0.6'
|
implementation 'ch.qos.logback:logback-classic:1.4.14'
|
||||||
testImplementation 'org.springframework.boot:spring-boot-starter-test:3.0.6'
|
implementation 'ch.qos.logback:logback-core:1.4.14'
|
||||||
// https://mvnrepository.com/artifact/org.apache.pdfbox/jbig2-imageio
|
implementation 'org.springframework:spring-webmvc:6.0.15'
|
||||||
implementation group: 'org.apache.pdfbox', name: 'jbig2-imageio', version: '3.0.4'
|
|
||||||
implementation 'commons-io:commons-io:2.11.0'
|
implementation 'org.yaml:snakeyaml:2.1'
|
||||||
|
implementation 'org.springframework.boot:spring-boot-starter-web:3.2.1'
|
||||||
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.1.0'
|
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf:3.2.1'
|
||||||
|
|
||||||
|
if (System.getenv('DOCKER_ENABLE_SECURITY') != 'false') {
|
||||||
|
implementation 'org.springframework.boot:spring-boot-starter-security:3.2.1'
|
||||||
|
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5:3.1.2.RELEASE'
|
||||||
|
implementation "org.springframework.boot:spring-boot-starter-data-jpa"
|
||||||
|
implementation "com.h2database:h2"
|
||||||
|
}
|
||||||
|
|
||||||
|
testImplementation 'org.springframework.boot:spring-boot-starter-test:3.2.1'
|
||||||
|
|
||||||
|
// Batik
|
||||||
|
implementation 'org.apache.xmlgraphics:batik-all:1.17'
|
||||||
|
|
||||||
|
// TwelveMonkeys
|
||||||
|
implementation 'com.twelvemonkeys.imageio:imageio-batik:3.10.1'
|
||||||
|
implementation 'com.twelvemonkeys.imageio:imageio-bmp:3.10.1'
|
||||||
|
// implementation 'com.twelvemonkeys.imageio:imageio-hdr:3.10.1'
|
||||||
|
// implementation 'com.twelvemonkeys.imageio:imageio-icns:3.10.1'
|
||||||
|
// implementation 'com.twelvemonkeys.imageio:imageio-iff:3.10.1'
|
||||||
|
implementation 'com.twelvemonkeys.imageio:imageio-jpeg:3.10.1'
|
||||||
|
// implementation 'com.twelvemonkeys.imageio:imageio-pcx:3.10.1'
|
||||||
|
// implementation 'com.twelvemonkeys.imageio:imageio-pict:3.10.1'
|
||||||
|
// implementation 'com.twelvemonkeys.imageio:imageio-pnm:3.10.1'
|
||||||
|
// implementation 'com.twelvemonkeys.imageio:imageio-psd:3.10.1'
|
||||||
|
// implementation 'com.twelvemonkeys.imageio:imageio-sgi:3.10.1'
|
||||||
|
// implementation 'com.twelvemonkeys.imageio:imageio-tga:3.10.1'
|
||||||
|
// implementation 'com.twelvemonkeys.imageio:imageio-thumbsdb:3.10.1'
|
||||||
|
implementation 'com.twelvemonkeys.imageio:imageio-tiff:3.10.1'
|
||||||
|
implementation 'com.twelvemonkeys.imageio:imageio-webp:3.10.1'
|
||||||
|
// implementation 'com.twelvemonkeys.imageio:imageio-xwd:3.10.1'
|
||||||
|
|
||||||
|
implementation 'commons-io:commons-io:2.15.1'
|
||||||
|
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0'
|
||||||
|
|
||||||
|
//general PDF
|
||||||
|
|
||||||
|
// https://mvnrepository.com/artifact/com.opencsv/opencsv
|
||||||
|
implementation ('com.opencsv:opencsv:5.7.1') {
|
||||||
|
exclude group: 'commons-logging', module: 'commons-logging'
|
||||||
|
}
|
||||||
|
|
||||||
//general PDF
|
implementation ('org.apache.pdfbox:pdfbox:2.0.29'){
|
||||||
implementation 'org.apache.pdfbox:pdfbox:2.0.28'
|
exclude group: 'commons-logging', module: 'commons-logging'
|
||||||
implementation 'org.bouncycastle:bcprov-jdk15on:1.70'
|
}
|
||||||
implementation 'org.bouncycastle:bcpkix-jdk15on:1.70'
|
|
||||||
implementation 'com.itextpdf:itext7-core:7.2.5'
|
implementation ('org.apache.pdfbox:xmpbox:2.0.29'){
|
||||||
|
exclude group: 'commons-logging', module: 'commons-logging'
|
||||||
|
}
|
||||||
|
|
||||||
|
implementation 'org.bouncycastle:bcprov-jdk18on:1.77'
|
||||||
|
implementation 'org.bouncycastle:bcpkix-jdk18on:1.77'
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-actuator'
|
implementation 'org.springframework.boot:spring-boot-starter-actuator'
|
||||||
implementation 'io.micrometer:micrometer-core'
|
implementation 'io.micrometer:micrometer-core'
|
||||||
|
implementation group: 'com.google.zxing', name: 'core', version: '3.5.2'
|
||||||
developmentOnly("org.springframework.boot:spring-boot-devtools")
|
// https://mvnrepository.com/artifact/org.commonmark/commonmark
|
||||||
|
implementation 'org.commonmark:commonmark:0.21.0'
|
||||||
|
// https://mvnrepository.com/artifact/com.github.vladimir-bukhtoyarov/bucket4j-core
|
||||||
|
implementation 'com.github.vladimir-bukhtoyarov:bucket4j-core:7.6.0'
|
||||||
|
|
||||||
|
developmentOnly("org.springframework.boot:spring-boot-devtools")
|
||||||
|
compileOnly 'org.projectlombok:lombok:1.18.30'
|
||||||
|
annotationProcessor 'org.projectlombok:lombok:1.18.28'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tasks.withType(JavaCompile) {
|
||||||
|
dependsOn 'spotlessApply'
|
||||||
|
}
|
||||||
|
|
||||||
|
task writeVersion {
|
||||||
|
def propsFile = file('src/main/resources/version.properties')
|
||||||
|
def props = new Properties()
|
||||||
|
props.setProperty('version', version)
|
||||||
|
props.store(propsFile.newWriter(), null)
|
||||||
|
}
|
||||||
|
|
||||||
|
swaggerhubUpload {
|
||||||
|
//dependsOn generateOpenApiDocs // Depends on your task generating Swagger docs
|
||||||
|
api 'Stirling-PDF' // The name of your API on SwaggerHub
|
||||||
|
owner 'Frooodle' // Your SwaggerHub username (or organization name)
|
||||||
|
version project.version // The version of your API
|
||||||
|
inputFile './SwaggerDoc.json' // The path to your Swagger docs
|
||||||
|
token "${System.getenv('SWAGGERHUB_API_KEY')}" // Your SwaggerHub API key, passed as an environment variable
|
||||||
|
oas '3.0.0' // The version of the OpenAPI Specification you're using
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
jar {
|
jar {
|
||||||
enabled = false
|
enabled = false
|
||||||
manifest {
|
manifest {
|
||||||
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/Frooodle/Stirling-PDF
|
||||||
|
keywords:
|
||||||
|
- stirling-pdf
|
||||||
|
- helm
|
||||||
|
- charts repo
|
||||||
|
maintainers:
|
||||||
|
- name: Frooodle
|
||||||
|
url: https://github.com/Frooodle/Stirling-PDF
|
||||||
|
name: stirling-pdf-chart
|
||||||
|
sources:
|
||||||
|
- https://github.com/Frooodle/Stirling-PDF
|
||||||
|
version: 1.0.0
|
||||||
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:
|
||||||
|
|
||||||
32
groups.md
32
groups.md
@@ -1,32 +0,0 @@
|
|||||||
Operation | PageOps | Convert | Security | Other | CLI | Python | OpenCV | LibreOffice | OCRmyPDF | Java | Javascript
|
|
||||||
--------------------|---------|---------|----------|-------|------|--------|--------|-------------|--------- |-------- |-----------
|
|
||||||
remove-pages | X | | | | | | | | | X |
|
|
||||||
merge-pdfs | X | | | | | | | | | X |
|
|
||||||
split-pdfs | X | | | | | | | | | X |
|
|
||||||
pdf-organizer | X | | | | | | | | | X | X
|
|
||||||
rotate-pdf | X | | | | | | | | | X |
|
|
||||||
pdf-to-img | | X | | | | | | | | X |
|
|
||||||
img-to-pdf | | X | | | | | | | | X |
|
|
||||||
pdf-to-pdfa | | X | | | X | | | | X | |
|
|
||||||
file-to-pdf | | X | | | X | | | X | | |
|
|
||||||
xlsx-to-pdf | | X | | | X | | | X | | |
|
|
||||||
pdf-to-word | | X | | | X | | | X | | |
|
|
||||||
pdf-to-presentation | | X | | | X | | | X | | |
|
|
||||||
pdf-to-text | | X | | | X | | | X | | |
|
|
||||||
pdf-to-html | | X | | | X | | | X | | |
|
|
||||||
pdf-to-xml | | X | | | X | | | X | | |
|
|
||||||
add-password | | | X | | | | | | | X |
|
|
||||||
remove-password | | | X | | | | | | | X |
|
|
||||||
change-permissions | | | X | | | | | | | X |
|
|
||||||
add-watermark | | | X | | | | | | | X |
|
|
||||||
ocr-pdf | | | | X | X | | | | X | |
|
|
||||||
add-image | | | | X | | | | | | X |
|
|
||||||
compress-pdf | | | | X | X | | | | X
|
|
||||||
extract-images | | | | X | | | | | | X |
|
|
||||||
change-metadata | | | | X | | | | | | X |
|
|
||||||
extract-image-scans | | | | X | X | X | X | | | |
|
|
||||||
sign | | | | X | | | | | | | X
|
|
||||||
flatten | | | | X | | | | | | |
|
|
||||||
repair | | | | X | X | | | X | | |
|
|
||||||
remove-blanks | | | | X | X | X | X | | | |
|
|
||||||
compare | | | | X | | | | | | | X
|
|
||||||
BIN
images/login-dark.png
Normal file
BIN
images/login-dark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
BIN
images/login-light.png
Normal file
BIN
images/login-light.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 128 KiB After Width: | Height: | Size: 131 KiB |
36
lauch4jConfig.xml
Normal file
36
lauch4jConfig.xml
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<launch4jConfig>
|
||||||
|
<dontWrapJar>false</dontWrapJar>
|
||||||
|
<headerType>console</headerType>
|
||||||
|
<jar>.\build\libs\S-PDF-0.10.1.jar</jar>
|
||||||
|
<outfile>.\Stirling-PDF.exe</outfile>
|
||||||
|
<errTitle>Please download Java17</errTitle>
|
||||||
|
<cmdLine></cmdLine>
|
||||||
|
<chdir>.</chdir>
|
||||||
|
<priority>normal</priority>
|
||||||
|
<downloadUrl>https://download.oracle.com/java/17/latest/jdk-17_windows-x64_bin.exe</downloadUrl>
|
||||||
|
<supportUrl></supportUrl>
|
||||||
|
<stayAlive>false</stayAlive>
|
||||||
|
<restartOnCrash>false</restartOnCrash>
|
||||||
|
<manifest></manifest>
|
||||||
|
<icon>./src/main/resources/static/favicon.ico</icon>
|
||||||
|
<var>BROWSER_OPEN=true</var>
|
||||||
|
<singleInstance>
|
||||||
|
<mutexName>Stirling-PDF</mutexName>
|
||||||
|
<windowTitle>Stirling-PDF</windowTitle>
|
||||||
|
</singleInstance>
|
||||||
|
<jre>
|
||||||
|
<path>%JAVA_HOME%;%PATH%</path>
|
||||||
|
<requiresJdk>false</requiresJdk>
|
||||||
|
<requires64Bit>false</requires64Bit>
|
||||||
|
<minVersion>17</minVersion>
|
||||||
|
<maxVersion></maxVersion>
|
||||||
|
</jre>
|
||||||
|
<messages>
|
||||||
|
<startupErr>An error occurred while starting Stirling-PDF</startupErr>
|
||||||
|
<jreNotFoundErr>This application requires a Java Runtime Environment, Please download Java 17.</jreNotFoundErr>
|
||||||
|
<jreVersionErr>You are running the wrong version of Java, Please download Java 17.</jreVersionErr>
|
||||||
|
<launcherErr>Java is corrupted. Please uninstall and then install Java 17.</launcherErr>
|
||||||
|
<instanceAlreadyExistsMsg>Stirling-PDF is already running.</instanceAlreadyExistsMsg>
|
||||||
|
</messages>
|
||||||
|
</launch4jConfig>
|
||||||
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}"
|
||||||
|
}
|
||||||
80
scripts/PropSync.java
Normal file
80
scripts/PropSync.java
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
package stirling.software.Stirling.Stats;
|
||||||
|
|
||||||
|
import java.nio.file.*;
|
||||||
|
import java.nio.charset.MalformedInputException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.io.*;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
public class PropSync {
|
||||||
|
|
||||||
|
public static void main(String[] args) throws IOException {
|
||||||
|
File folder = new File("C:\\Users\\systo\\git\\Stirling-PDF\\src\\main\\resources");
|
||||||
|
File[] files = folder.listFiles((dir, name) -> name.matches("messages_.*\\.properties"));
|
||||||
|
|
||||||
|
List<String> enLines = Files.readAllLines(Paths.get(folder + "\\messages_en_GB.properties"), StandardCharsets.UTF_8);
|
||||||
|
Map<String, String> enProps = linesToProps(enLines);
|
||||||
|
|
||||||
|
for (File file : files) {
|
||||||
|
if (!file.getName().equals("messages_en_GB.properties")) {
|
||||||
|
System.out.println("Processing file: " + file.getName());
|
||||||
|
List<String> lines;
|
||||||
|
try {
|
||||||
|
lines = Files.readAllLines(file.toPath(), StandardCharsets.UTF_8);
|
||||||
|
} catch (MalformedInputException e) {
|
||||||
|
System.out.println("Skipping due to not UTF8 format for file: " + file.getName());
|
||||||
|
continue;
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new UncheckedIOException(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, String> currentProps = linesToProps(lines);
|
||||||
|
List<String> newLines = syncPropsWithLines(enProps, currentProps, enLines);
|
||||||
|
|
||||||
|
Files.write(file.toPath(), newLines, StandardCharsets.UTF_8);
|
||||||
|
System.out.println("Finished processing file: " + file.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, String> linesToProps(List<String> lines) {
|
||||||
|
Map<String, String> props = new LinkedHashMap<>();
|
||||||
|
for (String line : lines) {
|
||||||
|
if (!line.trim().isEmpty() && line.contains("=")) {
|
||||||
|
String[] parts = line.split("=", 2);
|
||||||
|
props.put(parts[0].trim(), parts[1].trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return props;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<String> syncPropsWithLines(Map<String, String> enProps, Map<String, String> currentProps, List<String> enLines) {
|
||||||
|
List<String> newLines = new ArrayList<>();
|
||||||
|
boolean needsTranslateComment = false; // flag to check if we need to add "TODO: Translate"
|
||||||
|
|
||||||
|
for (String line : enLines) {
|
||||||
|
if (line.contains("=")) {
|
||||||
|
String key = line.split("=", 2)[0].trim();
|
||||||
|
|
||||||
|
if (currentProps.containsKey(key)) {
|
||||||
|
newLines.add(key + "=" + currentProps.get(key));
|
||||||
|
needsTranslateComment = false;
|
||||||
|
} else {
|
||||||
|
if (!needsTranslateComment) {
|
||||||
|
newLines.add("##########################");
|
||||||
|
newLines.add("### TODO: Translate ###");
|
||||||
|
newLines.add("##########################");
|
||||||
|
needsTranslateComment = true;
|
||||||
|
}
|
||||||
|
newLines.add(line);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// handle comments and other non-property lines
|
||||||
|
newLines.add(line);
|
||||||
|
needsTranslateComment = false; // reset the flag when we encounter comments or empty lines
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newLines;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -14,13 +14,13 @@ def is_blank_image(image_path, threshold=10, white_percent=99, white_value=255,
|
|||||||
blurred_image = cv2.GaussianBlur(image, (blur_size, blur_size), 0)
|
blurred_image = cv2.GaussianBlur(image, (blur_size, blur_size), 0)
|
||||||
|
|
||||||
_, 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 = np.sum(thresholded_image == white_value)
|
white_pixels = np.sum(thresholded_image == white_value)
|
||||||
total_pixels = thresholded_image.size
|
white_pixel_percentage = (white_pixels / thresholded_image.size) * 100
|
||||||
white_pixel_percentage = (white_pixels / total_pixels) * 100
|
|
||||||
print(f"Page has white pixel percent of {white_pixel_percentage}")
|
print(f"Page has white pixel percent of {white_pixel_percentage}")
|
||||||
return white_pixel_percentage > white_percent
|
return white_pixel_percentage >= white_percent
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
@@ -32,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/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
|
||||||
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 "$@"
|
||||||
@@ -3,7 +3,24 @@
|
|||||||
# 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/tesseract-ocr
|
||||||
cp -rn /usr/share/tesseract-ocr-original/* /usr/share/tesseract-ocr
|
cp -rn /usr/share/tesseract-ocr-original/* /usr/share/tesseract-ocr
|
||||||
|
|
||||||
|
if [ -d /usr/share/tesseract-ocr/4.00/tessdata ]; then
|
||||||
|
cp -r /usr/share/tesseract-ocr/4.00/tessdata/* /usr/share/tesseract-ocr/5/tessdata/ || true;
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if TESSERACT_LANGS environment variable is set and is not empty
|
||||||
|
if [[ -n "$TESSERACT_LANGS" ]]; then
|
||||||
|
# Convert comma-separated values to a space-separated list
|
||||||
|
LANGS=$(echo $TESSERACT_LANGS | tr ',' ' ')
|
||||||
|
|
||||||
|
# Install each language pack
|
||||||
|
for LANG in $LANGS; do
|
||||||
|
apt-get install -y "tesseract-ocr-$LANG"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
/scripts/download-security-jar.sh
|
||||||
|
|
||||||
# Run the main command
|
# Run the main command
|
||||||
exec "$@"
|
exec "$@"
|
||||||
@@ -1 +1 @@
|
|||||||
rootProject.name = 'S-PDF'
|
rootProject.name = 'Stirling-PDF'
|
||||||
|
|||||||
@@ -22,14 +22,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) {
|
||||||
@@ -49,21 +49,22 @@ public class LibreOfficeListener {
|
|||||||
|
|
||||||
// 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 +93,4 @@ public class LibreOfficeListener {
|
|||||||
process.destroy();
|
process.destroy();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,81 @@
|
|||||||
package stirling.software.SPDF;
|
package stirling.software.SPDF;
|
||||||
|
|
||||||
import org.springframework.boot.SpringApplication;
|
import java.nio.file.Files;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import java.nio.file.Paths;
|
||||||
|
import java.util.Collections;
|
||||||
@SpringBootApplication
|
|
||||||
public class SPdfApplication {
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
public static void main(String[] args) {
|
import org.springframework.boot.SpringApplication;
|
||||||
SpringApplication.run(SPdfApplication.class, args);
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
}
|
import org.springframework.core.env.Environment;
|
||||||
}
|
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||||
|
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
|
import stirling.software.SPDF.config.ConfigInitializer;
|
||||||
|
import stirling.software.SPDF.utils.GeneralUtils;
|
||||||
|
|
||||||
|
@SpringBootApplication
|
||||||
|
@EnableScheduling
|
||||||
|
public class SPdfApplication {
|
||||||
|
|
||||||
|
@Autowired private Environment env;
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void init() {
|
||||||
|
// Check if the BROWSER_OPEN environment variable is set to true
|
||||||
|
String browserOpenEnv = env.getProperty("BROWSER_OPEN");
|
||||||
|
boolean browserOpen = browserOpenEnv != null && browserOpenEnv.equalsIgnoreCase("true");
|
||||||
|
|
||||||
|
if (browserOpen) {
|
||||||
|
try {
|
||||||
|
String url = "http://localhost:" + getPort();
|
||||||
|
|
||||||
|
String os = System.getProperty("os.name").toLowerCase();
|
||||||
|
Runtime rt = Runtime.getRuntime();
|
||||||
|
if (os.contains("win")) {
|
||||||
|
// For Windows
|
||||||
|
rt.exec("rundll32 url.dll,FileProtocolHandler " + url);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SpringApplication app = new SpringApplication(SPdfApplication.class);
|
||||||
|
app.addInitializers(new ConfigInitializer());
|
||||||
|
if (Files.exists(Paths.get("configs/settings.yml"))) {
|
||||||
|
app.setDefaultProperties(
|
||||||
|
Collections.singletonMap(
|
||||||
|
"spring.config.additional-location", "file:configs/settings.yml"));
|
||||||
|
} else {
|
||||||
|
System.out.println(
|
||||||
|
"External configuration file 'configs/settings.yml' does not exist. Using default configuration and environment configuration instead.");
|
||||||
|
}
|
||||||
|
app.run(args);
|
||||||
|
|
||||||
|
try {
|
||||||
|
Thread.sleep(1000);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
// TODO Auto-generated catch block
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
GeneralUtils.createDir("customFiles/static/");
|
||||||
|
GeneralUtils.createDir("customFiles/templates/");
|
||||||
|
|
||||||
|
System.out.println("Stirling-PDF Started.");
|
||||||
|
|
||||||
|
String url = "http://localhost:" + getPort();
|
||||||
|
System.out.println("Navigate to " + url);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getPort() {
|
||||||
|
String port = System.getProperty("local.server.port");
|
||||||
|
if (port == null || port.isEmpty()) {
|
||||||
|
port = "8080";
|
||||||
|
}
|
||||||
|
return port;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,42 +1,60 @@
|
|||||||
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;
|
||||||
@Configuration
|
|
||||||
public class AppConfig {
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
@Bean(name = "appName")
|
|
||||||
public String appName() {
|
@Configuration
|
||||||
String appName = System.getProperty("APP_HOME_NAME");
|
public class AppConfig {
|
||||||
if (appName == null)
|
|
||||||
appName = System.getenv("APP_HOME_NAME");
|
@Autowired ApplicationProperties applicationProperties;
|
||||||
return (appName != null) ? appName : "Stirling PDF";
|
|
||||||
}
|
@Bean(name = "loginEnabled")
|
||||||
|
public boolean loginEnabled() {
|
||||||
@Bean(name = "appVersion")
|
return applicationProperties.getSecurity().getEnableLogin();
|
||||||
public String appVersion() {
|
}
|
||||||
String version = getClass().getPackage().getImplementationVersion();
|
|
||||||
return (version != null) ? version : "0.3.3";
|
@Bean(name = "appName")
|
||||||
}
|
public String appName() {
|
||||||
|
String homeTitle = applicationProperties.getUi().getAppName();
|
||||||
@Bean(name = "homeText")
|
return (homeTitle != null) ? homeTitle : "Stirling PDF";
|
||||||
public String homeText() {
|
}
|
||||||
String homeText = System.getProperty("APP_HOME_DESCRIPTION");
|
|
||||||
if (homeText == null)
|
@Bean(name = "appVersion")
|
||||||
homeText = System.getenv("APP_HOME_DESCRIPTION");
|
public String appVersion() {
|
||||||
return (homeText != null) ? homeText : "null";
|
String version = getClass().getPackage().getImplementationVersion();
|
||||||
}
|
return (version != null) ? version : "0.0.0";
|
||||||
|
}
|
||||||
@Bean(name = "navBarText")
|
|
||||||
public String navBarText() {
|
@Bean(name = "homeText")
|
||||||
String navBarText = System.getProperty("APP_NAVBAR_NAME");
|
public String homeText() {
|
||||||
if (navBarText == null)
|
return (applicationProperties.getUi().getHomeDescription() != null)
|
||||||
navBarText = System.getenv("APP_NAVBAR_NAME");
|
? applicationProperties.getUi().getHomeDescription()
|
||||||
if (navBarText == null)
|
: "null";
|
||||||
navBarText = System.getProperty("APP_HOME_NAME");
|
}
|
||||||
if (navBarText == null)
|
|
||||||
navBarText = System.getenv("APP_HOME_NAME");
|
@Bean(name = "navBarText")
|
||||||
|
public String navBarText() {
|
||||||
return (navBarText != null) ? navBarText : "Stirling PDF";
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,53 +1,64 @@
|
|||||||
package stirling.software.SPDF.config;
|
package stirling.software.SPDF.config;
|
||||||
|
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
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.web.servlet.LocaleResolver;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
import org.springframework.web.servlet.LocaleResolver;
|
||||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||||
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
import org.springframework.web.servlet.i18n.SessionLocaleResolver;
|
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
|
||||||
|
import org.springframework.web.servlet.i18n.SessionLocaleResolver;
|
||||||
@Configuration
|
|
||||||
public class Beans implements WebMvcConfigurer {
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
|
|
||||||
@Override
|
@Configuration
|
||||||
public void addInterceptors(InterceptorRegistry registry) {
|
public class Beans implements WebMvcConfigurer {
|
||||||
registry.addInterceptor(localeChangeInterceptor());
|
|
||||||
registry.addInterceptor(new CleanUrlInterceptor());
|
@Autowired ApplicationProperties applicationProperties;
|
||||||
}
|
|
||||||
|
@Override
|
||||||
@Bean
|
public void addInterceptors(InterceptorRegistry registry) {
|
||||||
public LocaleChangeInterceptor localeChangeInterceptor() {
|
registry.addInterceptor(localeChangeInterceptor());
|
||||||
LocaleChangeInterceptor lci = new LocaleChangeInterceptor();
|
registry.addInterceptor(new CleanUrlInterceptor());
|
||||||
lci.setParamName("lang");
|
}
|
||||||
return lci;
|
|
||||||
}
|
@Bean
|
||||||
|
public LocaleChangeInterceptor localeChangeInterceptor() {
|
||||||
@Bean
|
LocaleChangeInterceptor lci = new LocaleChangeInterceptor();
|
||||||
public LocaleResolver localeResolver() {
|
lci.setParamName("lang");
|
||||||
SessionLocaleResolver slr = new SessionLocaleResolver();
|
return lci;
|
||||||
|
}
|
||||||
String appLocaleEnv = System.getProperty("APP_LOCALE");
|
|
||||||
if (appLocaleEnv == null)
|
@Bean
|
||||||
appLocaleEnv = System.getenv("APP_LOCALE");
|
public LocaleResolver localeResolver() {
|
||||||
Locale defaultLocale = Locale.UK; // Fallback to UK locale if environment variable is not set
|
SessionLocaleResolver slr = new SessionLocaleResolver();
|
||||||
|
|
||||||
if (appLocaleEnv != null && !appLocaleEnv.isEmpty()) {
|
String appLocaleEnv = applicationProperties.getSystem().getDefaultLocale();
|
||||||
Locale tempLocale = Locale.forLanguageTag(appLocaleEnv);
|
Locale defaultLocale =
|
||||||
String tempLanguageTag = tempLocale.toLanguageTag();
|
Locale.UK; // Fallback to UK locale if environment variable is not set
|
||||||
|
|
||||||
if (appLocaleEnv.equalsIgnoreCase(tempLanguageTag)) {
|
if (appLocaleEnv != null && !appLocaleEnv.isEmpty()) {
|
||||||
defaultLocale = tempLocale;
|
Locale tempLocale = Locale.forLanguageTag(appLocaleEnv);
|
||||||
} else {
|
String tempLanguageTag = tempLocale.toLanguageTag();
|
||||||
System.err.println("Invalid APP_LOCALE environment variable value. Falling back to default Locale.UK.");
|
|
||||||
}
|
if (appLocaleEnv.equalsIgnoreCase(tempLanguageTag)) {
|
||||||
}
|
defaultLocale = tempLocale;
|
||||||
|
} else {
|
||||||
slr.setDefaultLocale(defaultLocale);
|
tempLocale = Locale.forLanguageTag(appLocaleEnv.replace("_", "-"));
|
||||||
return slr;
|
tempLanguageTag = tempLocale.toLanguageTag();
|
||||||
}
|
|
||||||
|
if (appLocaleEnv.equalsIgnoreCase(tempLanguageTag)) {
|
||||||
}
|
defaultLocale = tempLocale;
|
||||||
|
} else {
|
||||||
|
System.err.println(
|
||||||
|
"Invalid APP_LOCALE environment variable value. Falling back to default Locale.UK.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
slr.setDefaultLocale(defaultLocale);
|
||||||
|
return slr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,45 +1,74 @@
|
|||||||
package stirling.software.SPDF.config;
|
package stirling.software.SPDF.config;
|
||||||
import java.util.regex.Matcher;
|
|
||||||
import java.util.regex.Pattern;
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
import org.springframework.web.servlet.HandlerInterceptor;
|
import java.util.List;
|
||||||
import org.springframework.web.servlet.ModelAndView;
|
import java.util.Map;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import org.springframework.web.servlet.HandlerInterceptor;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import org.springframework.web.servlet.ModelAndView;
|
||||||
|
|
||||||
public class CleanUrlInterceptor implements HandlerInterceptor {
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
private static final Pattern LANG_PATTERN = Pattern.compile("&?lang=([^&]+)");
|
|
||||||
|
public class CleanUrlInterceptor implements HandlerInterceptor {
|
||||||
@Override
|
|
||||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
|
private static final List<String> ALLOWED_PARAMS =
|
||||||
String queryString = request.getQueryString();
|
Arrays.asList(
|
||||||
if (queryString != null && !queryString.isEmpty()) {
|
"lang", "endpoint", "endpoints", "logout", "error", "file", "messageType");
|
||||||
String requestURI = request.getRequestURI();
|
|
||||||
|
@Override
|
||||||
// Keep the lang parameter if it exists
|
public boolean preHandle(
|
||||||
Matcher langMatcher = LANG_PATTERN.matcher(queryString);
|
HttpServletRequest request, HttpServletResponse response, Object handler)
|
||||||
String langQueryString = langMatcher.find() ? "lang=" + langMatcher.group(1) : "";
|
throws Exception {
|
||||||
|
String queryString = request.getQueryString();
|
||||||
// Check if there are any other query parameters besides the lang parameter
|
if (queryString != null && !queryString.isEmpty()) {
|
||||||
String remainingQueryString = queryString.replaceAll(LANG_PATTERN.pattern(), "").replaceAll("&+", "&").replaceAll("^&|&$", "");
|
String requestURI = request.getRequestURI();
|
||||||
|
Map<String, String> parameters = new HashMap<>();
|
||||||
if (!remainingQueryString.isEmpty()) {
|
|
||||||
// Redirect to the URL without other query parameters
|
// Keep only the allowed parameters
|
||||||
String redirectUrl = requestURI + (langQueryString.isEmpty() ? "" : "?" + langQueryString);
|
String[] queryParameters = queryString.split("&");
|
||||||
response.sendRedirect(redirectUrl);
|
for (String param : queryParameters) {
|
||||||
return false;
|
String[] keyValue = param.split("=");
|
||||||
}
|
if (keyValue.length != 2) {
|
||||||
}
|
continue;
|
||||||
return true;
|
}
|
||||||
}
|
if (ALLOWED_PARAMS.contains(keyValue[0])) {
|
||||||
|
parameters.put(keyValue[0], keyValue[1]);
|
||||||
@Override
|
}
|
||||||
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {
|
}
|
||||||
}
|
|
||||||
|
// If there are any parameters that are not allowed
|
||||||
@Override
|
if (parameters.size() != queryParameters.length) {
|
||||||
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
|
// Construct new query string
|
||||||
}
|
StringBuilder newQueryString = new StringBuilder();
|
||||||
}
|
for (Map.Entry<String, String> entry : parameters.entrySet()) {
|
||||||
|
if (newQueryString.length() > 0) {
|
||||||
|
newQueryString.append("&");
|
||||||
|
}
|
||||||
|
newQueryString.append(entry.getKey()).append("=").append(entry.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirect to the URL with only allowed query parameters
|
||||||
|
String redirectUrl = requestURI + "?" + newQueryString;
|
||||||
|
response.sendRedirect(redirectUrl);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void postHandle(
|
||||||
|
HttpServletRequest request,
|
||||||
|
HttpServletResponse response,
|
||||||
|
Object handler,
|
||||||
|
ModelAndView modelAndView) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterCompletion(
|
||||||
|
HttpServletRequest request,
|
||||||
|
HttpServletResponse response,
|
||||||
|
Object handler,
|
||||||
|
Exception ex) {}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,143 @@
|
|||||||
|
package stirling.software.SPDF.config;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import org.springframework.context.ApplicationContextInitializer;
|
||||||
|
import org.springframework.context.ConfigurableApplicationContext;
|
||||||
|
|
||||||
|
public class ConfigInitializer
|
||||||
|
implements ApplicationContextInitializer<ConfigurableApplicationContext> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initialize(ConfigurableApplicationContext applicationContext) {
|
||||||
|
try {
|
||||||
|
ensureConfigExists();
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException("Failed to initialize application configuration", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ensureConfigExists() throws IOException {
|
||||||
|
// Define the path to the external config directory
|
||||||
|
Path destPath = Paths.get("configs", "settings.yml");
|
||||||
|
|
||||||
|
// Check if the file already exists
|
||||||
|
if (Files.notExists(destPath)) {
|
||||||
|
// Ensure the destination directory exists
|
||||||
|
Files.createDirectories(destPath.getParent());
|
||||||
|
|
||||||
|
// Copy the resource from classpath to the external directory
|
||||||
|
try (InputStream in =
|
||||||
|
getClass().getClassLoader().getResourceAsStream("settings.yml.template")) {
|
||||||
|
if (in != null) {
|
||||||
|
Files.copy(in, destPath);
|
||||||
|
} else {
|
||||||
|
throw new FileNotFoundException(
|
||||||
|
"Resource file not found: settings.yml.template");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If user file exists, we need to merge it with the template from the classpath
|
||||||
|
List<String> templateLines;
|
||||||
|
try (InputStream in =
|
||||||
|
getClass().getClassLoader().getResourceAsStream("settings.yml.template")) {
|
||||||
|
templateLines =
|
||||||
|
new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))
|
||||||
|
.lines()
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
mergeYamlFiles(templateLines, destPath, destPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void mergeYamlFiles(List<String> templateLines, Path userFilePath, Path outputPath)
|
||||||
|
throws IOException {
|
||||||
|
List<String> userLines = Files.readAllLines(userFilePath);
|
||||||
|
List<String> mergedLines = new ArrayList<>();
|
||||||
|
boolean insideAutoGenerated = false;
|
||||||
|
boolean beforeFirstKey = true;
|
||||||
|
|
||||||
|
Function<String, Boolean> isCommented = line -> line.trim().startsWith("#");
|
||||||
|
Function<String, String> extractKey =
|
||||||
|
line -> {
|
||||||
|
String[] parts = line.split(":");
|
||||||
|
return parts.length > 0 ? parts[0].trim().replace("#", "").trim() : "";
|
||||||
|
};
|
||||||
|
|
||||||
|
Set<String> userKeys = userLines.stream().map(extractKey).collect(Collectors.toSet());
|
||||||
|
|
||||||
|
for (String line : templateLines) {
|
||||||
|
String key = extractKey.apply(line);
|
||||||
|
|
||||||
|
if (line.trim().equalsIgnoreCase("AutomaticallyGenerated:")) {
|
||||||
|
insideAutoGenerated = true;
|
||||||
|
mergedLines.add(line);
|
||||||
|
continue;
|
||||||
|
} else if (insideAutoGenerated && line.trim().isEmpty()) {
|
||||||
|
insideAutoGenerated = false;
|
||||||
|
mergedLines.add(line);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (beforeFirstKey && (isCommented.apply(line) || line.trim().isEmpty())) {
|
||||||
|
// Handle top comments and empty lines before the first key.
|
||||||
|
mergedLines.add(line);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!key.isEmpty()) beforeFirstKey = false;
|
||||||
|
|
||||||
|
if (userKeys.contains(key)) {
|
||||||
|
// If user has any version (commented or uncommented) of this key, skip the
|
||||||
|
// template line
|
||||||
|
Optional<String> userValue =
|
||||||
|
userLines.stream()
|
||||||
|
.filter(
|
||||||
|
l ->
|
||||||
|
extractKey.apply(l).equalsIgnoreCase(key)
|
||||||
|
&& !isCommented.apply(l))
|
||||||
|
.findFirst();
|
||||||
|
if (userValue.isPresent()) mergedLines.add(userValue.get());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCommented.apply(line) || line.trim().isEmpty() || !userKeys.contains(key)) {
|
||||||
|
mergedLines.add(
|
||||||
|
line); // If line is commented, empty or key not present in user's file,
|
||||||
|
// retain the
|
||||||
|
// template line
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add any additional uncommented user lines that are not present in the
|
||||||
|
// template
|
||||||
|
for (String userLine : userLines) {
|
||||||
|
String userKey = extractKey.apply(userLine);
|
||||||
|
boolean isPresentInTemplate =
|
||||||
|
templateLines.stream()
|
||||||
|
.map(extractKey)
|
||||||
|
.anyMatch(templateKey -> templateKey.equalsIgnoreCase(userKey));
|
||||||
|
if (!isPresentInTemplate && !isCommented.apply(userLine)) {
|
||||||
|
mergedLines.add(userLine);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Files.write(outputPath, mergedLines, StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,190 +1,231 @@
|
|||||||
package stirling.software.SPDF.config;
|
package stirling.software.SPDF.config;
|
||||||
|
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Map;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Map;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.Logger;
|
||||||
import org.springframework.stereotype.Service;
|
import org.slf4j.LoggerFactory;
|
||||||
@Service
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
public class EndpointConfiguration {
|
import org.springframework.stereotype.Service;
|
||||||
private static final Logger logger = LoggerFactory.getLogger(EndpointConfiguration.class);
|
|
||||||
private Map<String, Boolean> endpointStatuses = new ConcurrentHashMap<>();
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
private Map<String, Set<String>> endpointGroups = new ConcurrentHashMap<>();
|
|
||||||
|
@Service
|
||||||
public EndpointConfiguration() {
|
public class EndpointConfiguration {
|
||||||
init();
|
private static final Logger logger = LoggerFactory.getLogger(EndpointConfiguration.class);
|
||||||
processEnvironmentConfigs();
|
private Map<String, Boolean> endpointStatuses = new ConcurrentHashMap<>();
|
||||||
}
|
private Map<String, Set<String>> endpointGroups = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
public void enableEndpoint(String endpoint) {
|
private final ApplicationProperties applicationProperties;
|
||||||
endpointStatuses.put(endpoint, true);
|
|
||||||
}
|
@Autowired
|
||||||
|
public EndpointConfiguration(ApplicationProperties applicationProperties) {
|
||||||
public void disableEndpoint(String endpoint) {
|
this.applicationProperties = applicationProperties;
|
||||||
logger.info("Disabling {}", endpoint);
|
init();
|
||||||
endpointStatuses.put(endpoint, false);
|
processEnvironmentConfigs();
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isEndpointEnabled(String endpoint) {
|
public void enableEndpoint(String endpoint) {
|
||||||
if (endpoint.startsWith("/")) {
|
endpointStatuses.put(endpoint, true);
|
||||||
endpoint = endpoint.substring(1);
|
}
|
||||||
}
|
|
||||||
return endpointStatuses.getOrDefault(endpoint, true);
|
public void disableEndpoint(String endpoint) {
|
||||||
}
|
if (!endpointStatuses.containsKey(endpoint) || endpointStatuses.get(endpoint) != false) {
|
||||||
|
logger.info("Disabling {}", endpoint);
|
||||||
public void addEndpointToGroup(String group, String endpoint) {
|
endpointStatuses.put(endpoint, false);
|
||||||
endpointGroups.computeIfAbsent(group, k -> new HashSet<>()).add(endpoint);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void enableGroup(String group) {
|
public boolean isEndpointEnabled(String endpoint) {
|
||||||
Set<String> endpoints = endpointGroups.get(group);
|
if (endpoint.startsWith("/")) {
|
||||||
if (endpoints != null) {
|
endpoint = endpoint.substring(1);
|
||||||
for (String endpoint : endpoints) {
|
}
|
||||||
enableEndpoint(endpoint);
|
return endpointStatuses.getOrDefault(endpoint, true);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
public void addEndpointToGroup(String group, String endpoint) {
|
||||||
|
endpointGroups.computeIfAbsent(group, k -> new HashSet<>()).add(endpoint);
|
||||||
public void disableGroup(String group) {
|
}
|
||||||
Set<String> endpoints = endpointGroups.get(group);
|
|
||||||
if (endpoints != null) {
|
public void enableGroup(String group) {
|
||||||
for (String endpoint : endpoints) {
|
Set<String> endpoints = endpointGroups.get(group);
|
||||||
disableEndpoint(endpoint);
|
if (endpoints != null) {
|
||||||
}
|
for (String endpoint : endpoints) {
|
||||||
}
|
enableEndpoint(endpoint);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
public void init() {
|
}
|
||||||
// Adding endpoints to "PageOps" group
|
|
||||||
addEndpointToGroup("PageOps", "remove-pages");
|
public void disableGroup(String group) {
|
||||||
addEndpointToGroup("PageOps", "merge-pdfs");
|
Set<String> endpoints = endpointGroups.get(group);
|
||||||
addEndpointToGroup("PageOps", "split-pdfs");
|
if (endpoints != null) {
|
||||||
addEndpointToGroup("PageOps", "pdf-organizer");
|
for (String endpoint : endpoints) {
|
||||||
addEndpointToGroup("PageOps", "rotate-pdf");
|
disableEndpoint(endpoint);
|
||||||
|
}
|
||||||
// Adding endpoints to "Convert" group
|
}
|
||||||
addEndpointToGroup("Convert", "pdf-to-img");
|
}
|
||||||
addEndpointToGroup("Convert", "img-to-pdf");
|
|
||||||
addEndpointToGroup("Convert", "pdf-to-pdfa");
|
public void init() {
|
||||||
addEndpointToGroup("Convert", "file-to-pdf");
|
// Adding endpoints to "PageOps" group
|
||||||
addEndpointToGroup("Convert", "xlsx-to-pdf");
|
addEndpointToGroup("PageOps", "remove-pages");
|
||||||
addEndpointToGroup("Convert", "pdf-to-word");
|
addEndpointToGroup("PageOps", "merge-pdfs");
|
||||||
addEndpointToGroup("Convert", "pdf-to-presentation");
|
addEndpointToGroup("PageOps", "split-pdfs");
|
||||||
addEndpointToGroup("Convert", "pdf-to-text");
|
addEndpointToGroup("PageOps", "pdf-organizer");
|
||||||
addEndpointToGroup("Convert", "pdf-to-html");
|
addEndpointToGroup("PageOps", "rotate-pdf");
|
||||||
addEndpointToGroup("Convert", "pdf-to-xml");
|
addEndpointToGroup("PageOps", "multi-page-layout");
|
||||||
|
addEndpointToGroup("PageOps", "scale-pages");
|
||||||
// Adding endpoints to "Security" group
|
addEndpointToGroup("PageOps", "adjust-contrast");
|
||||||
addEndpointToGroup("Security", "add-password");
|
addEndpointToGroup("PageOps", "crop");
|
||||||
addEndpointToGroup("Security", "remove-password");
|
addEndpointToGroup("PageOps", "auto-split-pdf");
|
||||||
addEndpointToGroup("Security", "change-permissions");
|
addEndpointToGroup("PageOps", "extract-page");
|
||||||
addEndpointToGroup("Security", "add-watermark");
|
addEndpointToGroup("PageOps", "pdf-to-single-page");
|
||||||
|
addEndpointToGroup("PageOps", "split-by-size-or-count");
|
||||||
// Adding endpoints to "Other" group
|
addEndpointToGroup("PageOps", "overlay-pdf");
|
||||||
addEndpointToGroup("Other", "ocr-pdf");
|
addEndpointToGroup("PageOps", "split-pdf-by-sections");
|
||||||
addEndpointToGroup("Other", "add-image");
|
|
||||||
addEndpointToGroup("Other", "compress-pdf");
|
// Adding endpoints to "Convert" group
|
||||||
addEndpointToGroup("Other", "extract-images");
|
addEndpointToGroup("Convert", "pdf-to-img");
|
||||||
addEndpointToGroup("Other", "change-metadata");
|
addEndpointToGroup("Convert", "img-to-pdf");
|
||||||
addEndpointToGroup("Other", "extract-image-scans");
|
addEndpointToGroup("Convert", "pdf-to-pdfa");
|
||||||
addEndpointToGroup("Other", "sign");
|
addEndpointToGroup("Convert", "file-to-pdf");
|
||||||
addEndpointToGroup("Other", "flatten");
|
addEndpointToGroup("Convert", "xlsx-to-pdf");
|
||||||
addEndpointToGroup("Other", "repair");
|
addEndpointToGroup("Convert", "pdf-to-word");
|
||||||
addEndpointToGroup("Other", "remove-blanks");
|
addEndpointToGroup("Convert", "pdf-to-presentation");
|
||||||
addEndpointToGroup("Other", "compare");
|
addEndpointToGroup("Convert", "pdf-to-text");
|
||||||
|
addEndpointToGroup("Convert", "pdf-to-html");
|
||||||
|
addEndpointToGroup("Convert", "pdf-to-xml");
|
||||||
|
addEndpointToGroup("Convert", "html-to-pdf");
|
||||||
|
addEndpointToGroup("Convert", "url-to-pdf");
|
||||||
|
addEndpointToGroup("Convert", "markdown-to-pdf");
|
||||||
|
addEndpointToGroup("Convert", "pdf-to-csv");
|
||||||
|
|
||||||
//CLI
|
// Adding endpoints to "Security" group
|
||||||
addEndpointToGroup("CLI", "compress-pdf");
|
addEndpointToGroup("Security", "add-password");
|
||||||
addEndpointToGroup("CLI", "extract-image-scans");
|
addEndpointToGroup("Security", "remove-password");
|
||||||
addEndpointToGroup("CLI", "remove-blanks");
|
addEndpointToGroup("Security", "change-permissions");
|
||||||
addEndpointToGroup("CLI", "repair");
|
addEndpointToGroup("Security", "add-watermark");
|
||||||
addEndpointToGroup("CLI", "pdf-to-pdfa");
|
addEndpointToGroup("Security", "cert-sign");
|
||||||
addEndpointToGroup("CLI", "file-to-pdf");
|
addEndpointToGroup("Security", "sanitize-pdf");
|
||||||
addEndpointToGroup("CLI", "xlsx-to-pdf");
|
addEndpointToGroup("Security", "auto-redact");
|
||||||
addEndpointToGroup("CLI", "pdf-to-word");
|
|
||||||
addEndpointToGroup("CLI", "pdf-to-presentation");
|
// Adding endpoints to "Other" group
|
||||||
addEndpointToGroup("CLI", "pdf-to-text");
|
addEndpointToGroup("Other", "ocr-pdf");
|
||||||
addEndpointToGroup("CLI", "pdf-to-html");
|
addEndpointToGroup("Other", "add-image");
|
||||||
addEndpointToGroup("CLI", "pdf-to-xml");
|
addEndpointToGroup("Other", "compress-pdf");
|
||||||
addEndpointToGroup("CLI", "ocr-pdf");
|
addEndpointToGroup("Other", "extract-images");
|
||||||
|
addEndpointToGroup("Other", "change-metadata");
|
||||||
//python
|
addEndpointToGroup("Other", "extract-image-scans");
|
||||||
addEndpointToGroup("Python", "extract-image-scans");
|
addEndpointToGroup("Other", "sign");
|
||||||
addEndpointToGroup("Python", "remove-blanks");
|
addEndpointToGroup("Other", "flatten");
|
||||||
|
addEndpointToGroup("Other", "repair");
|
||||||
|
addEndpointToGroup("Other", "remove-blanks");
|
||||||
|
addEndpointToGroup("Other", "remove-annotations");
|
||||||
//openCV
|
addEndpointToGroup("Other", "compare");
|
||||||
addEndpointToGroup("OpenCV", "extract-image-scans");
|
addEndpointToGroup("Other", "add-page-numbers");
|
||||||
addEndpointToGroup("OpenCV", "remove-blanks");
|
addEndpointToGroup("Other", "auto-rename");
|
||||||
|
addEndpointToGroup("Other", "get-info-on-pdf");
|
||||||
//LibreOffice
|
addEndpointToGroup("Other", "show-javascript");
|
||||||
addEndpointToGroup("LibreOffice", "repair");
|
|
||||||
addEndpointToGroup("LibreOffice", "file-to-pdf");
|
// CLI
|
||||||
addEndpointToGroup("LibreOffice", "xlsx-to-pdf");
|
addEndpointToGroup("CLI", "compress-pdf");
|
||||||
addEndpointToGroup("LibreOffice", "pdf-to-word");
|
addEndpointToGroup("CLI", "extract-image-scans");
|
||||||
addEndpointToGroup("LibreOffice", "pdf-to-presentation");
|
addEndpointToGroup("CLI", "remove-blanks");
|
||||||
addEndpointToGroup("LibreOffice", "pdf-to-text");
|
addEndpointToGroup("CLI", "repair");
|
||||||
addEndpointToGroup("LibreOffice", "pdf-to-html");
|
addEndpointToGroup("CLI", "pdf-to-pdfa");
|
||||||
addEndpointToGroup("LibreOffice", "pdf-to-xml");
|
addEndpointToGroup("CLI", "file-to-pdf");
|
||||||
|
addEndpointToGroup("CLI", "xlsx-to-pdf");
|
||||||
|
addEndpointToGroup("CLI", "pdf-to-word");
|
||||||
//OCRmyPDF
|
addEndpointToGroup("CLI", "pdf-to-presentation");
|
||||||
addEndpointToGroup("OCRmyPDF", "compress-pdf");
|
addEndpointToGroup("CLI", "pdf-to-text");
|
||||||
addEndpointToGroup("OCRmyPDF", "pdf-to-pdfa");
|
addEndpointToGroup("CLI", "pdf-to-html");
|
||||||
addEndpointToGroup("OCRmyPDF", "ocr-pdf");
|
addEndpointToGroup("CLI", "pdf-to-xml");
|
||||||
|
addEndpointToGroup("CLI", "ocr-pdf");
|
||||||
//Java
|
addEndpointToGroup("CLI", "html-to-pdf");
|
||||||
addEndpointToGroup("Java", "merge-pdfs");
|
addEndpointToGroup("CLI", "url-to-pdf");
|
||||||
addEndpointToGroup("Java", "remove-pages");
|
|
||||||
addEndpointToGroup("Java", "split-pdfs");
|
// python
|
||||||
addEndpointToGroup("Java", "pdf-organizer");
|
addEndpointToGroup("Python", "extract-image-scans");
|
||||||
addEndpointToGroup("Java", "rotate-pdf");
|
addEndpointToGroup("Python", "remove-blanks");
|
||||||
addEndpointToGroup("Java", "pdf-to-img");
|
addEndpointToGroup("Python", "html-to-pdf");
|
||||||
addEndpointToGroup("Java", "img-to-pdf");
|
addEndpointToGroup("Python", "url-to-pdf");
|
||||||
addEndpointToGroup("Java", "add-password");
|
|
||||||
addEndpointToGroup("Java", "remove-password");
|
// openCV
|
||||||
addEndpointToGroup("Java", "change-permissions");
|
addEndpointToGroup("OpenCV", "extract-image-scans");
|
||||||
addEndpointToGroup("Java", "add-watermark");
|
addEndpointToGroup("OpenCV", "remove-blanks");
|
||||||
addEndpointToGroup("Java", "add-image");
|
|
||||||
addEndpointToGroup("Java", "extract-images");
|
// LibreOffice
|
||||||
addEndpointToGroup("Java", "change-metadata");
|
addEndpointToGroup("LibreOffice", "repair");
|
||||||
|
addEndpointToGroup("LibreOffice", "file-to-pdf");
|
||||||
|
addEndpointToGroup("LibreOffice", "xlsx-to-pdf");
|
||||||
//Javascript
|
addEndpointToGroup("LibreOffice", "pdf-to-word");
|
||||||
addEndpointToGroup("Javascript", "pdf-organizer");
|
addEndpointToGroup("LibreOffice", "pdf-to-presentation");
|
||||||
addEndpointToGroup("Javascript", "sign");
|
addEndpointToGroup("LibreOffice", "pdf-to-text");
|
||||||
addEndpointToGroup("Javascript", "compare");
|
addEndpointToGroup("LibreOffice", "pdf-to-html");
|
||||||
|
addEndpointToGroup("LibreOffice", "pdf-to-xml");
|
||||||
}
|
|
||||||
|
// OCRmyPDF
|
||||||
private void processEnvironmentConfigs() {
|
addEndpointToGroup("OCRmyPDF", "compress-pdf");
|
||||||
String endpointsToRemove = System.getenv("ENDPOINTS_TO_REMOVE");
|
addEndpointToGroup("OCRmyPDF", "pdf-to-pdfa");
|
||||||
String groupsToRemove = System.getenv("GROUPS_TO_REMOVE");
|
addEndpointToGroup("OCRmyPDF", "ocr-pdf");
|
||||||
|
|
||||||
if (endpointsToRemove != null) {
|
// Java
|
||||||
String[] endpoints = endpointsToRemove.split(",");
|
addEndpointToGroup("Java", "merge-pdfs");
|
||||||
for (String endpoint : endpoints) {
|
addEndpointToGroup("Java", "remove-pages");
|
||||||
disableEndpoint(endpoint.trim());
|
addEndpointToGroup("Java", "split-pdfs");
|
||||||
}
|
addEndpointToGroup("Java", "pdf-organizer");
|
||||||
}
|
addEndpointToGroup("Java", "rotate-pdf");
|
||||||
|
addEndpointToGroup("Java", "pdf-to-img");
|
||||||
if (groupsToRemove != null) {
|
addEndpointToGroup("Java", "img-to-pdf");
|
||||||
String[] groups = groupsToRemove.split(",");
|
addEndpointToGroup("Java", "add-password");
|
||||||
for (String group : groups) {
|
addEndpointToGroup("Java", "remove-password");
|
||||||
disableGroup(group.trim());
|
addEndpointToGroup("Java", "change-permissions");
|
||||||
}
|
addEndpointToGroup("Java", "add-watermark");
|
||||||
}
|
addEndpointToGroup("Java", "add-image");
|
||||||
}
|
addEndpointToGroup("Java", "extract-images");
|
||||||
|
addEndpointToGroup("Java", "change-metadata");
|
||||||
}
|
addEndpointToGroup("Java", "cert-sign");
|
||||||
|
addEndpointToGroup("Java", "multi-page-layout");
|
||||||
|
addEndpointToGroup("Java", "scale-pages");
|
||||||
|
addEndpointToGroup("Java", "add-page-numbers");
|
||||||
|
addEndpointToGroup("Java", "auto-rename");
|
||||||
|
addEndpointToGroup("Java", "auto-split-pdf");
|
||||||
|
addEndpointToGroup("Java", "sanitize-pdf");
|
||||||
|
addEndpointToGroup("Java", "crop");
|
||||||
|
addEndpointToGroup("Java", "get-info-on-pdf");
|
||||||
|
addEndpointToGroup("Java", "extract-page");
|
||||||
|
addEndpointToGroup("Java", "pdf-to-single-page");
|
||||||
|
addEndpointToGroup("Java", "markdown-to-pdf");
|
||||||
|
addEndpointToGroup("Java", "show-javascript");
|
||||||
|
addEndpointToGroup("Java", "auto-redact");
|
||||||
|
addEndpointToGroup("Java", "pdf-to-csv");
|
||||||
|
addEndpointToGroup("Java", "split-by-size-or-count");
|
||||||
|
addEndpointToGroup("Java", "overlay-pdf");
|
||||||
|
addEndpointToGroup("Java", "split-pdf-by-sections");
|
||||||
|
|
||||||
|
// Javascript
|
||||||
|
addEndpointToGroup("Javascript", "pdf-organizer");
|
||||||
|
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 (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,46 +1,63 @@
|
|||||||
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 {
|
||||||
// Ignore static resources
|
String uri = request.getRequestURI();
|
||||||
if (!(uri.startsWith("/js") || uri.startsWith("/images") || uri.endsWith(".ico") || uri.endsWith(".css") || uri.endsWith(".svg")|| uri.endsWith(".js") || uri.contains("swagger") || uri.startsWith("/api"))) {
|
|
||||||
Counter counter = Counter.builder("http.requests")
|
// System.out.println("uri="+uri + ", method=" + request.getMethod() );
|
||||||
.tag("uri", uri)
|
// Ignore static resources
|
||||||
.tag("method", request.getMethod())
|
if (!(uri.startsWith("/js")
|
||||||
.register(meterRegistry);
|
|| uri.startsWith("/v1/api-docs")
|
||||||
|
|| uri.endsWith("robots.txt")
|
||||||
counter.increment();
|
|| uri.startsWith("/images")
|
||||||
}
|
|| uri.startsWith("/images")
|
||||||
|
|| uri.endsWith(".png")
|
||||||
filterChain.doFilter(request, response);
|
|| uri.endsWith(".ico")
|
||||||
}
|
|| uri.endsWith(".css")
|
||||||
|
|| uri.endsWith(".map")
|
||||||
|
|| uri.endsWith(".svg")
|
||||||
|
|| uri.endsWith(".js")
|
||||||
}
|
|| uri.contains("swagger")
|
||||||
|
|| uri.startsWith("/api/v1/info")
|
||||||
|
|| uri.startsWith("/site.webmanifest")
|
||||||
|
|| uri.startsWith("/fonts")
|
||||||
|
|| uri.startsWith("/pdfjs"))) {
|
||||||
|
|
||||||
|
Counter counter =
|
||||||
|
Counter.builder("http.requests")
|
||||||
|
.tag("uri", uri)
|
||||||
|
.tag("method", request.getMethod())
|
||||||
|
.register(meterRegistry);
|
||||||
|
|
||||||
|
counter.increment();
|
||||||
|
}
|
||||||
|
|
||||||
|
filterChain.doFilter(request, response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,22 +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
|
||||||
version = (version != null) ? version : "1.0.0";
|
public class OpenApiConfig {
|
||||||
|
|
||||||
return new OpenAPI().components(new Components()).info(
|
@Autowired ApplicationProperties applicationProperties;
|
||||||
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."));
|
|
||||||
}
|
@Bean
|
||||||
|
public OpenAPI customOpenAPI() {
|
||||||
}
|
String version = getClass().getPackage().getImplementationVersion();
|
||||||
|
if (version == null) {
|
||||||
|
version = "1.0.0"; // default version if all else fails
|
||||||
|
}
|
||||||
|
|
||||||
|
SecurityScheme apiKeyScheme =
|
||||||
|
new SecurityScheme()
|
||||||
|
.type(SecurityScheme.Type.APIKEY)
|
||||||
|
.in(SecurityScheme.In.HEADER)
|
||||||
|
.name("X-API-KEY");
|
||||||
|
if (!applicationProperties.getSecurity().getEnableLogin()) {
|
||||||
|
return new OpenAPI()
|
||||||
|
.components(new Components())
|
||||||
|
.info(
|
||||||
|
new Info()
|
||||||
|
.title("Stirling PDF API")
|
||||||
|
.version(version)
|
||||||
|
.description(
|
||||||
|
"API documentation for all Server-Side processing.\nPlease note some functionality might be UI only and missing from here."));
|
||||||
|
} else {
|
||||||
|
return new OpenAPI()
|
||||||
|
.components(new Components().addSecuritySchemes("apiKey", apiKeyScheme))
|
||||||
|
.info(
|
||||||
|
new Info()
|
||||||
|
.title("Stirling PDF API")
|
||||||
|
.version(version)
|
||||||
|
.description(
|
||||||
|
"API documentation for all Server-Side processing.\nPlease note some functionality might be UI only and missing from here."))
|
||||||
|
.addSecurityItem(new SecurityRequirement().addList("apiKey"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package stirling.software.SPDF.config;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
import org.springframework.context.ApplicationListener;
|
||||||
|
import org.springframework.context.event.ContextRefreshedEvent;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class StartupApplicationListener implements ApplicationListener<ContextRefreshedEvent> {
|
||||||
|
|
||||||
|
public static LocalDateTime startTime;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onApplicationEvent(ContextRefreshedEvent event) {
|
||||||
|
startTime = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,18 +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.WebMvcConfigurer;
|
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||||
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
@Configuration
|
|
||||||
public class WebMvcConfig implements WebMvcConfigurer {
|
@Configuration
|
||||||
|
public class WebMvcConfig implements WebMvcConfigurer {
|
||||||
@Autowired
|
|
||||||
private EndpointInterceptor endpointInterceptor;
|
@Autowired private EndpointInterceptor endpointInterceptor;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void addInterceptors(InterceptorRegistry registry) {
|
public void addInterceptors(InterceptorRegistry registry) {
|
||||||
registry.addInterceptor(endpointInterceptor);
|
registry.addInterceptor(endpointInterceptor);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
@Override
|
||||||
|
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||||
|
// Handler for external static resources
|
||||||
|
registry.addResourceHandler("/**")
|
||||||
|
.addResourceLocations("file:customFiles/static/", "classpath:/static/");
|
||||||
|
// .setCachePeriod(0); // Optional: disable caching
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package stirling.software.SPDF.config;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Properties;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.config.YamlPropertiesFactoryBean;
|
||||||
|
import org.springframework.core.env.PropertiesPropertySource;
|
||||||
|
import org.springframework.core.env.PropertySource;
|
||||||
|
import org.springframework.core.io.support.EncodedResource;
|
||||||
|
import org.springframework.core.io.support.PropertySourceFactory;
|
||||||
|
|
||||||
|
public class YamlPropertySourceFactory implements PropertySourceFactory {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PropertySource<?> createPropertySource(String name, EncodedResource encodedResource)
|
||||||
|
throws IOException {
|
||||||
|
YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
|
||||||
|
factory.setResources(encodedResource.getResource());
|
||||||
|
|
||||||
|
Properties properties = factory.getObject();
|
||||||
|
|
||||||
|
return new PropertiesPropertySource(
|
||||||
|
encodedResource.getResource().getFilename(), properties);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package stirling.software.SPDF.config.security;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.security.authentication.BadCredentialsException;
|
||||||
|
import org.springframework.security.authentication.LockedException;
|
||||||
|
import org.springframework.security.core.AuthenticationException;
|
||||||
|
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
|
||||||
|
|
||||||
|
@Autowired private final LoginAttemptService loginAttemptService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public CustomAuthenticationFailureHandler(LoginAttemptService loginAttemptService) {
|
||||||
|
this.loginAttemptService = loginAttemptService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAuthenticationFailure(
|
||||||
|
HttpServletRequest request,
|
||||||
|
HttpServletResponse response,
|
||||||
|
AuthenticationException exception)
|
||||||
|
throws IOException, ServletException {
|
||||||
|
String ip = request.getRemoteAddr();
|
||||||
|
logger.error("Failed login attempt from IP: " + ip);
|
||||||
|
|
||||||
|
String username = request.getParameter("username");
|
||||||
|
if (loginAttemptService.loginAttemptCheck(username)) {
|
||||||
|
setDefaultFailureUrl("/login?error=locked");
|
||||||
|
|
||||||
|
} else {
|
||||||
|
if (exception.getClass().isAssignableFrom(BadCredentialsException.class)) {
|
||||||
|
setDefaultFailureUrl("/login?error=badcredentials");
|
||||||
|
} else if (exception.getClass().isAssignableFrom(LockedException.class)) {
|
||||||
|
setDefaultFailureUrl("/login?error=locked");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
super.onAuthenticationFailure(request, response, exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package stirling.software.SPDF.config.security;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.security.authentication.LockedException;
|
||||||
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||||
|
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.model.Authority;
|
||||||
|
import stirling.software.SPDF.model.User;
|
||||||
|
import stirling.software.SPDF.repository.UserRepository;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class CustomUserDetailsService implements UserDetailsService {
|
||||||
|
|
||||||
|
@Autowired private UserRepository userRepository;
|
||||||
|
|
||||||
|
@Autowired private LoginAttemptService loginAttemptService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
|
||||||
|
User user =
|
||||||
|
userRepository
|
||||||
|
.findByUsername(username)
|
||||||
|
.orElseThrow(
|
||||||
|
() ->
|
||||||
|
new UsernameNotFoundException(
|
||||||
|
"No user found with username: " + username));
|
||||||
|
|
||||||
|
if (loginAttemptService.isBlocked(username)) {
|
||||||
|
throw new LockedException(
|
||||||
|
"Your account has been locked due to too many failed login attempts.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new org.springframework.security.core.userdetails.User(
|
||||||
|
user.getUsername(),
|
||||||
|
user.getPassword(),
|
||||||
|
user.isEnabled(),
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
getAuthorities(user.getAuthorities()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Collection<? extends GrantedAuthority> getAuthorities(Set<Authority> authorities) {
|
||||||
|
return authorities.stream()
|
||||||
|
.map(authority -> new SimpleGrantedAuthority(authority.getAuthority()))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package stirling.software.SPDF.config.security;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.filter.OncePerRequestFilter;
|
||||||
|
|
||||||
|
import jakarta.servlet.FilterChain;
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import stirling.software.SPDF.model.User;
|
||||||
|
import stirling.software.SPDF.utils.RequestUriUtils;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class FirstLoginFilter extends OncePerRequestFilter {
|
||||||
|
|
||||||
|
@Autowired @Lazy private UserService userService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doFilterInternal(
|
||||||
|
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||||
|
throws ServletException, IOException {
|
||||||
|
String method = request.getMethod();
|
||||||
|
String requestURI = request.getRequestURI();
|
||||||
|
// Check if the request is for static resources
|
||||||
|
boolean isStaticResource = RequestUriUtils.isStaticResource(requestURI);
|
||||||
|
|
||||||
|
// If it's a static resource, just continue the filter chain and skip the logic below
|
||||||
|
if (isStaticResource) {
|
||||||
|
filterChain.doFilter(request, response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||||
|
if (authentication != null && authentication.isAuthenticated()) {
|
||||||
|
Optional<User> user = userService.findByUsername(authentication.getName());
|
||||||
|
if ("GET".equalsIgnoreCase(method)
|
||||||
|
&& user.isPresent()
|
||||||
|
&& user.get().isFirstLogin()
|
||||||
|
&& !"/change-creds".equals(requestURI)) {
|
||||||
|
response.sendRedirect("/change-creds");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
filterChain.doFilter(request, response);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
package stirling.software.SPDF.config.security;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
|
import stirling.software.SPDF.model.Role;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class InitialSecuritySetup {
|
||||||
|
|
||||||
|
@Autowired private UserService userService;
|
||||||
|
|
||||||
|
@Autowired ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void init() {
|
||||||
|
if (!userService.hasUsers()) {
|
||||||
|
|
||||||
|
String initialUsername =
|
||||||
|
applicationProperties.getSecurity().getInitialLogin().getUsername();
|
||||||
|
String initialPassword =
|
||||||
|
applicationProperties.getSecurity().getInitialLogin().getPassword();
|
||||||
|
if (initialUsername != null && initialPassword != null) {
|
||||||
|
userService.saveUser(initialUsername, initialPassword, Role.ADMIN.getRoleId());
|
||||||
|
} else {
|
||||||
|
initialUsername = "admin";
|
||||||
|
initialPassword = "stirling";
|
||||||
|
userService.saveUser(
|
||||||
|
initialUsername, initialPassword, Role.ADMIN.getRoleId(), true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!userService.usernameExists(Role.INTERNAL_API_USER.getRoleId())) {
|
||||||
|
userService.saveUser(
|
||||||
|
Role.INTERNAL_API_USER.getRoleId(),
|
||||||
|
UUID.randomUUID().toString(),
|
||||||
|
Role.INTERNAL_API_USER.getRoleId());
|
||||||
|
userService.addApiKeyToUser(Role.INTERNAL_API_USER.getRoleId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void initSecretKey() throws IOException {
|
||||||
|
String secretKey = applicationProperties.getAutomaticallyGenerated().getKey();
|
||||||
|
if (secretKey == null || secretKey.isEmpty()) {
|
||||||
|
secretKey = UUID.randomUUID().toString(); // Generating a random UUID as the secret key
|
||||||
|
saveKeyToConfig(secretKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveKeyToConfig(String key) throws IOException {
|
||||||
|
Path path = Paths.get("configs", "settings.yml"); // Target the configs/settings.yml
|
||||||
|
List<String> lines = Files.readAllLines(path);
|
||||||
|
boolean keyFound = false;
|
||||||
|
|
||||||
|
// Search for the existing key to replace it or place to add it
|
||||||
|
for (int i = 0; i < lines.size(); i++) {
|
||||||
|
if (lines.get(i).startsWith("AutomaticallyGenerated:")) {
|
||||||
|
keyFound = true;
|
||||||
|
if (i + 1 < lines.size() && lines.get(i + 1).trim().startsWith("key:")) {
|
||||||
|
lines.set(i + 1, " key: " + key);
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
lines.add(i + 1, " key: " + key);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the section doesn't exist, append it
|
||||||
|
if (!keyFound) {
|
||||||
|
lines.add("# Automatically Generated Settings (Do Not Edit Directly)");
|
||||||
|
lines.add("AutomaticallyGenerated:");
|
||||||
|
lines.add(" key: " + key);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write back to the file
|
||||||
|
Files.write(path, lines);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
package stirling.software.SPDF.config.security;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
|
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
|
||||||
|
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||||
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
|
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||||
|
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
|
||||||
|
import org.springframework.security.web.savedrequest.NullRequestCache;
|
||||||
|
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.repository.JPATokenRepositoryImpl;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableWebSecurity()
|
||||||
|
@EnableMethodSecurity
|
||||||
|
public class SecurityConfiguration {
|
||||||
|
|
||||||
|
@Autowired private UserDetailsService userDetailsService;
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public PasswordEncoder passwordEncoder() {
|
||||||
|
return new BCryptPasswordEncoder();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Autowired @Lazy private UserService userService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
@Qualifier("loginEnabled")
|
||||||
|
public boolean loginEnabledValue;
|
||||||
|
|
||||||
|
@Autowired private UserAuthenticationFilter userAuthenticationFilter;
|
||||||
|
|
||||||
|
@Autowired private LoginAttemptService loginAttemptService;
|
||||||
|
|
||||||
|
@Autowired private FirstLoginFilter firstLoginFilter;
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||||
|
http.addFilterBefore(userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
||||||
|
|
||||||
|
if (loginEnabledValue) {
|
||||||
|
|
||||||
|
http.csrf(csrf -> csrf.disable());
|
||||||
|
http.addFilterBefore(rateLimitingFilter(), UsernamePasswordAuthenticationFilter.class);
|
||||||
|
http.addFilterAfter(firstLoginFilter, UsernamePasswordAuthenticationFilter.class);
|
||||||
|
http.formLogin(
|
||||||
|
formLogin ->
|
||||||
|
formLogin
|
||||||
|
.loginPage("/login")
|
||||||
|
.successHandler(
|
||||||
|
new CustomAuthenticationSuccessHandler())
|
||||||
|
.defaultSuccessUrl("/")
|
||||||
|
.failureHandler(
|
||||||
|
new CustomAuthenticationFailureHandler(
|
||||||
|
loginAttemptService))
|
||||||
|
.permitAll())
|
||||||
|
.requestCache(requestCache -> requestCache.requestCache(new NullRequestCache()))
|
||||||
|
.logout(
|
||||||
|
logout ->
|
||||||
|
logout.logoutRequestMatcher(
|
||||||
|
new AntPathRequestMatcher("/logout"))
|
||||||
|
.logoutSuccessUrl("/login?logout=true")
|
||||||
|
.invalidateHttpSession(true) // Invalidate session
|
||||||
|
.deleteCookies("JSESSIONID", "remember-me"))
|
||||||
|
.rememberMe(
|
||||||
|
rememberMeConfigurer ->
|
||||||
|
rememberMeConfigurer // Use the configurator directly
|
||||||
|
.key("uniqueAndSecret")
|
||||||
|
.tokenRepository(persistentTokenRepository())
|
||||||
|
.tokenValiditySeconds(1209600) // 2 weeks
|
||||||
|
)
|
||||||
|
.authorizeHttpRequests(
|
||||||
|
authz ->
|
||||||
|
authz.requestMatchers(
|
||||||
|
req -> {
|
||||||
|
String uri = req.getRequestURI();
|
||||||
|
String contextPath = req.getContextPath();
|
||||||
|
|
||||||
|
// Remove the context path from the URI
|
||||||
|
String trimmedUri =
|
||||||
|
uri.startsWith(contextPath)
|
||||||
|
? uri.substring(
|
||||||
|
contextPath
|
||||||
|
.length())
|
||||||
|
: uri;
|
||||||
|
|
||||||
|
return trimmedUri.startsWith("/login")
|
||||||
|
|| trimmedUri.endsWith(".svg")
|
||||||
|
|| trimmedUri.startsWith(
|
||||||
|
"/register")
|
||||||
|
|| trimmedUri.startsWith("/error")
|
||||||
|
|| trimmedUri.startsWith("/images/")
|
||||||
|
|| trimmedUri.startsWith("/public/")
|
||||||
|
|| trimmedUri.startsWith("/css/")
|
||||||
|
|| trimmedUri.startsWith("/js/");
|
||||||
|
})
|
||||||
|
.permitAll()
|
||||||
|
.anyRequest()
|
||||||
|
.authenticated())
|
||||||
|
.userDetailsService(userDetailsService)
|
||||||
|
.authenticationProvider(authenticationProvider());
|
||||||
|
} else {
|
||||||
|
http.csrf(csrf -> csrf.disable())
|
||||||
|
.authorizeHttpRequests(authz -> authz.anyRequest().permitAll());
|
||||||
|
}
|
||||||
|
return http.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public IPRateLimitingFilter rateLimitingFilter() {
|
||||||
|
int maxRequestsPerIp = 1000000; // Example limit TODO add config level
|
||||||
|
return new IPRateLimitingFilter(maxRequestsPerIp, maxRequestsPerIp);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public DaoAuthenticationProvider authenticationProvider() {
|
||||||
|
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
|
||||||
|
authProvider.setUserDetailsService(userDetailsService);
|
||||||
|
authProvider.setPasswordEncoder(passwordEncoder());
|
||||||
|
return authProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public PersistentTokenRepository persistentTokenRepository() {
|
||||||
|
return new JPATokenRepositoryImpl();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
package stirling.software.SPDF.config.security;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.AuthenticationException;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.filter.OncePerRequestFilter;
|
||||||
|
|
||||||
|
import jakarta.servlet.FilterChain;
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import stirling.software.SPDF.model.ApiKeyAuthenticationToken;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class UserAuthenticationFilter extends OncePerRequestFilter {
|
||||||
|
|
||||||
|
@Autowired private UserDetailsService userDetailsService;
|
||||||
|
|
||||||
|
@Autowired @Lazy private UserService userService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
@Qualifier("loginEnabled")
|
||||||
|
public boolean loginEnabledValue;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doFilterInternal(
|
||||||
|
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||||
|
throws ServletException, IOException {
|
||||||
|
|
||||||
|
if (!loginEnabledValue) {
|
||||||
|
// If login is not enabled, just pass all requests without authentication
|
||||||
|
filterChain.doFilter(request, response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String requestURI = request.getRequestURI();
|
||||||
|
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||||
|
|
||||||
|
// Check for API key in the request headers if no authentication exists
|
||||||
|
if (authentication == null || !authentication.isAuthenticated()) {
|
||||||
|
String apiKey = request.getHeader("X-API-Key");
|
||||||
|
if (apiKey != null && !apiKey.trim().isEmpty()) {
|
||||||
|
try {
|
||||||
|
// Use API key to authenticate. This requires you to have an authentication
|
||||||
|
// provider for API keys.
|
||||||
|
UserDetails userDetails = userService.loadUserByApiKey(apiKey);
|
||||||
|
if (userDetails == null) {
|
||||||
|
response.setStatus(HttpStatus.UNAUTHORIZED.value());
|
||||||
|
response.getWriter().write("Invalid API Key.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
authentication =
|
||||||
|
new ApiKeyAuthenticationToken(
|
||||||
|
userDetails, apiKey, userDetails.getAuthorities());
|
||||||
|
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||||
|
} catch (AuthenticationException e) {
|
||||||
|
// If API key authentication fails, deny the request
|
||||||
|
response.setStatus(HttpStatus.UNAUTHORIZED.value());
|
||||||
|
response.getWriter().write("Invalid API Key.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we still don't have any authentication, deny the request
|
||||||
|
if (authentication == null || !authentication.isAuthenticated()) {
|
||||||
|
String method = request.getMethod();
|
||||||
|
String contextPath = request.getContextPath();
|
||||||
|
|
||||||
|
if ("GET".equalsIgnoreCase(method) && !(contextPath + "/login").equals(requestURI)) {
|
||||||
|
response.sendRedirect(contextPath + "/login"); // redirect to the login page
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
response.setStatus(HttpStatus.UNAUTHORIZED.value());
|
||||||
|
response.getWriter()
|
||||||
|
.write(
|
||||||
|
"Authentication required. Please provide a X-API-KEY in request header.\nThis is found in Settings -> Account Settings -> API Key\nAlternativly you can disable authentication if this is unexpected");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
filterChain.doFilter(request, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
|
||||||
|
String uri = request.getRequestURI();
|
||||||
|
String contextPath = request.getContextPath();
|
||||||
|
String[] permitAllPatterns = {
|
||||||
|
contextPath + "/login",
|
||||||
|
contextPath + "/register",
|
||||||
|
contextPath + "/error",
|
||||||
|
contextPath + "/images/",
|
||||||
|
contextPath + "/public/",
|
||||||
|
contextPath + "/css/",
|
||||||
|
contextPath + "/js/",
|
||||||
|
contextPath + "/pdfjs/",
|
||||||
|
contextPath + "/site.webmanifest"
|
||||||
|
};
|
||||||
|
|
||||||
|
for (String pattern : permitAllPatterns) {
|
||||||
|
if (uri.startsWith(pattern) || uri.endsWith(".svg")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
package stirling.software.SPDF.config.security;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.filter.OncePerRequestFilter;
|
||||||
|
|
||||||
|
import io.github.bucket4j.Bandwidth;
|
||||||
|
import io.github.bucket4j.Bucket;
|
||||||
|
import io.github.bucket4j.ConsumptionProbe;
|
||||||
|
import io.github.bucket4j.Refill;
|
||||||
|
|
||||||
|
import jakarta.servlet.FilterChain;
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import stirling.software.SPDF.model.Role;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class UserBasedRateLimitingFilter extends OncePerRequestFilter {
|
||||||
|
|
||||||
|
private final Map<String, Bucket> apiBuckets = new ConcurrentHashMap<>();
|
||||||
|
private final Map<String, Bucket> webBuckets = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
@Autowired private UserDetailsService userDetailsService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
@Qualifier("rateLimit")
|
||||||
|
public boolean rateLimit;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doFilterInternal(
|
||||||
|
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||||
|
throws ServletException, IOException {
|
||||||
|
if (!rateLimit) {
|
||||||
|
// If rateLimit is not enabled, just pass all requests without rate limiting
|
||||||
|
filterChain.doFilter(request, response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String method = request.getMethod();
|
||||||
|
if (!"POST".equalsIgnoreCase(method)) {
|
||||||
|
// If the request is not a POST, just pass it through without rate limiting
|
||||||
|
filterChain.doFilter(request, response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String identifier = null;
|
||||||
|
|
||||||
|
// Check for API key in the request headers
|
||||||
|
String apiKey = request.getHeader("X-API-Key");
|
||||||
|
if (apiKey != null && !apiKey.trim().isEmpty()) {
|
||||||
|
identifier =
|
||||||
|
"API_KEY_" + apiKey; // Prefix to distinguish between API keys and usernames
|
||||||
|
} else {
|
||||||
|
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||||
|
if (authentication != null && authentication.isAuthenticated()) {
|
||||||
|
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
|
||||||
|
identifier = userDetails.getUsername();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If neither API key nor an authenticated user is present, use IP address
|
||||||
|
if (identifier == null) {
|
||||||
|
identifier = request.getRemoteAddr();
|
||||||
|
}
|
||||||
|
|
||||||
|
Role userRole =
|
||||||
|
getRoleFromAuthentication(SecurityContextHolder.getContext().getAuthentication());
|
||||||
|
|
||||||
|
if (request.getHeader("X-API-Key") != null) {
|
||||||
|
// It's an API call
|
||||||
|
processRequest(
|
||||||
|
userRole.getApiCallsPerDay(),
|
||||||
|
identifier,
|
||||||
|
apiBuckets,
|
||||||
|
request,
|
||||||
|
response,
|
||||||
|
filterChain);
|
||||||
|
} else {
|
||||||
|
// It's a Web UI call
|
||||||
|
processRequest(
|
||||||
|
userRole.getWebCallsPerDay(),
|
||||||
|
identifier,
|
||||||
|
webBuckets,
|
||||||
|
request,
|
||||||
|
response,
|
||||||
|
filterChain);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Role getRoleFromAuthentication(Authentication authentication) {
|
||||||
|
if (authentication != null && authentication.isAuthenticated()) {
|
||||||
|
for (GrantedAuthority authority : authentication.getAuthorities()) {
|
||||||
|
try {
|
||||||
|
return Role.fromString(authority.getAuthority());
|
||||||
|
} catch (IllegalArgumentException ex) {
|
||||||
|
// Ignore and continue to next authority.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new IllegalStateException("User does not have a valid role.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processRequest(
|
||||||
|
int limitPerDay,
|
||||||
|
String identifier,
|
||||||
|
Map<String, Bucket> buckets,
|
||||||
|
HttpServletRequest request,
|
||||||
|
HttpServletResponse response,
|
||||||
|
FilterChain filterChain)
|
||||||
|
throws IOException, ServletException {
|
||||||
|
Bucket userBucket = buckets.computeIfAbsent(identifier, k -> createUserBucket(limitPerDay));
|
||||||
|
ConsumptionProbe probe = userBucket.tryConsumeAndReturnRemaining(1);
|
||||||
|
|
||||||
|
if (probe.isConsumed()) {
|
||||||
|
response.setHeader("X-Rate-Limit-Remaining", Long.toString(probe.getRemainingTokens()));
|
||||||
|
filterChain.doFilter(request, response);
|
||||||
|
} else {
|
||||||
|
long waitForRefill = probe.getNanosToWaitForRefill() / 1_000_000_000;
|
||||||
|
response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
|
||||||
|
response.setHeader("X-Rate-Limit-Retry-After-Seconds", String.valueOf(waitForRefill));
|
||||||
|
response.getWriter().write("Rate limit exceeded for POST requests.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Bucket createUserBucket(int limitPerDay) {
|
||||||
|
Bandwidth limit =
|
||||||
|
Bandwidth.classic(limitPerDay, Refill.intervally(limitPerDay, Duration.ofDays(1)));
|
||||||
|
return Bucket.builder().addLimit(limit).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
package stirling.software.SPDF.config.security;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.controller.api.pipeline.UserServiceInterface;
|
||||||
|
import stirling.software.SPDF.model.Authority;
|
||||||
|
import stirling.software.SPDF.model.Role;
|
||||||
|
import stirling.software.SPDF.model.User;
|
||||||
|
import stirling.software.SPDF.repository.UserRepository;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class UserService implements UserServiceInterface {
|
||||||
|
|
||||||
|
@Autowired private UserRepository userRepository;
|
||||||
|
|
||||||
|
@Autowired private PasswordEncoder passwordEncoder;
|
||||||
|
|
||||||
|
public Authentication getAuthentication(String apiKey) {
|
||||||
|
User user = getUserByApiKey(apiKey);
|
||||||
|
if (user == null) {
|
||||||
|
throw new UsernameNotFoundException("API key is not valid");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the user into an Authentication object
|
||||||
|
return new UsernamePasswordAuthenticationToken(
|
||||||
|
user, // principal (typically the user)
|
||||||
|
null, // credentials (we don't expose the password or API key here)
|
||||||
|
getAuthorities(user) // user's authorities (roles/permissions)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Collection<? extends GrantedAuthority> getAuthorities(User user) {
|
||||||
|
// Convert each Authority object into a SimpleGrantedAuthority object.
|
||||||
|
return user.getAuthorities().stream()
|
||||||
|
.map((Authority authority) -> new SimpleGrantedAuthority(authority.getAuthority()))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String generateApiKey() {
|
||||||
|
String apiKey;
|
||||||
|
do {
|
||||||
|
apiKey = UUID.randomUUID().toString();
|
||||||
|
} while (userRepository.findByApiKey(apiKey) != null); // Ensure uniqueness
|
||||||
|
return apiKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public User addApiKeyToUser(String username) {
|
||||||
|
User user =
|
||||||
|
userRepository
|
||||||
|
.findByUsername(username)
|
||||||
|
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
|
||||||
|
|
||||||
|
user.setApiKey(generateApiKey());
|
||||||
|
return userRepository.save(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
public User refreshApiKeyForUser(String username) {
|
||||||
|
return addApiKeyToUser(username); // reuse the add API key method for refreshing
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getApiKeyForUser(String username) {
|
||||||
|
User user =
|
||||||
|
userRepository
|
||||||
|
.findByUsername(username)
|
||||||
|
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
|
||||||
|
return user.getApiKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isValidApiKey(String apiKey) {
|
||||||
|
return userRepository.findByApiKey(apiKey) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public User getUserByApiKey(String apiKey) {
|
||||||
|
return userRepository.findByApiKey(apiKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
public UserDetails loadUserByApiKey(String apiKey) {
|
||||||
|
User userOptional = userRepository.findByApiKey(apiKey);
|
||||||
|
if (userOptional != null) {
|
||||||
|
User user = userOptional;
|
||||||
|
// Convert your User entity to a UserDetails object with authorities
|
||||||
|
return new org.springframework.security.core.userdetails.User(
|
||||||
|
user.getUsername(),
|
||||||
|
user.getPassword(), // you might not need this for API key auth
|
||||||
|
getAuthorities(user));
|
||||||
|
}
|
||||||
|
return null; // or throw an exception
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean validateApiKeyForUser(String username, String apiKey) {
|
||||||
|
Optional<User> userOpt = userRepository.findByUsername(username);
|
||||||
|
return userOpt.isPresent() && userOpt.get().getApiKey().equals(apiKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void saveUser(String username, String password) {
|
||||||
|
User user = new User();
|
||||||
|
user.setUsername(username);
|
||||||
|
user.setPassword(passwordEncoder.encode(password));
|
||||||
|
user.setEnabled(true);
|
||||||
|
userRepository.save(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void saveUser(String username, String password, String role, boolean firstLogin) {
|
||||||
|
User user = new User();
|
||||||
|
user.setUsername(username);
|
||||||
|
user.setPassword(passwordEncoder.encode(password));
|
||||||
|
user.addAuthority(new Authority(role, user));
|
||||||
|
user.setEnabled(true);
|
||||||
|
user.setFirstLogin(firstLogin);
|
||||||
|
userRepository.save(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void saveUser(String username, String password, String role) {
|
||||||
|
User user = new User();
|
||||||
|
user.setUsername(username);
|
||||||
|
user.setPassword(passwordEncoder.encode(password));
|
||||||
|
user.addAuthority(new Authority(role, user));
|
||||||
|
user.setEnabled(true);
|
||||||
|
user.setFirstLogin(false);
|
||||||
|
userRepository.save(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteUser(String username) {
|
||||||
|
Optional<User> userOpt = userRepository.findByUsername(username);
|
||||||
|
if (userOpt.isPresent()) {
|
||||||
|
for (Authority authority : userOpt.get().getAuthorities()) {
|
||||||
|
if (authority.getAuthority().equals(Role.INTERNAL_API_USER.getRoleId())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
userRepository.delete(userOpt.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean usernameExists(String username) {
|
||||||
|
return userRepository.findByUsername(username).isPresent();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasUsers() {
|
||||||
|
return userRepository.count() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateUserSettings(String username, Map<String, String> updates) {
|
||||||
|
Optional<User> userOpt = userRepository.findByUsername(username);
|
||||||
|
if (userOpt.isPresent()) {
|
||||||
|
User user = userOpt.get();
|
||||||
|
Map<String, String> settingsMap = user.getSettings();
|
||||||
|
|
||||||
|
if (settingsMap == null) {
|
||||||
|
settingsMap = new HashMap<String, String>();
|
||||||
|
}
|
||||||
|
settingsMap.clear();
|
||||||
|
settingsMap.putAll(updates);
|
||||||
|
user.setSettings(settingsMap);
|
||||||
|
|
||||||
|
userRepository.save(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<User> findByUsername(String username) {
|
||||||
|
return userRepository.findByUsername(username);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void changeUsername(User user, String newUsername) {
|
||||||
|
user.setUsername(newUsername);
|
||||||
|
userRepository.save(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void changePassword(User user, String newPassword) {
|
||||||
|
user.setPassword(passwordEncoder.encode(newPassword));
|
||||||
|
userRepository.save(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void changeFirstUse(User user, boolean firstUse) {
|
||||||
|
user.setFirstLogin(firstUse);
|
||||||
|
userRepository.save(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isPasswordCorrect(User user, String currentPassword) {
|
||||||
|
return passwordEncoder.matches(currentPassword, user.getPassword());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
package stirling.software.SPDF.controller.api;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import org.apache.pdfbox.multipdf.LayerUtility;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||||
|
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||||
|
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.model.api.general.CropPdfForm;
|
||||||
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/general")
|
||||||
|
@Tag(name = "General", description = "General APIs")
|
||||||
|
public class CropController {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(CropController.class);
|
||||||
|
|
||||||
|
@PostMapping(value = "/crop", consumes = "multipart/form-data")
|
||||||
|
@Operation(
|
||||||
|
summary = "Crops a PDF document",
|
||||||
|
description =
|
||||||
|
"This operation takes an input PDF file and crops it according to the given coordinates. Input:PDF Output:PDF Type:SISO")
|
||||||
|
public ResponseEntity<byte[]> cropPdf(@ModelAttribute CropPdfForm form) throws IOException {
|
||||||
|
|
||||||
|
PDDocument sourceDocument =
|
||||||
|
PDDocument.load(new ByteArrayInputStream(form.getFileInput().getBytes()));
|
||||||
|
|
||||||
|
PDDocument newDocument = new PDDocument();
|
||||||
|
|
||||||
|
int totalPages = sourceDocument.getNumberOfPages();
|
||||||
|
|
||||||
|
LayerUtility layerUtility = new LayerUtility(newDocument);
|
||||||
|
|
||||||
|
for (int i = 0; i < totalPages; i++) {
|
||||||
|
PDPage sourcePage = sourceDocument.getPage(i);
|
||||||
|
|
||||||
|
// Create a new page with the size of the source page
|
||||||
|
PDPage newPage = new PDPage(sourcePage.getMediaBox());
|
||||||
|
newDocument.addPage(newPage);
|
||||||
|
PDPageContentStream contentStream = new PDPageContentStream(newDocument, newPage);
|
||||||
|
|
||||||
|
// Import the source page as a form XObject
|
||||||
|
PDFormXObject formXObject = layerUtility.importPageAsForm(sourceDocument, i);
|
||||||
|
|
||||||
|
contentStream.saveGraphicsState();
|
||||||
|
|
||||||
|
// Define the crop area
|
||||||
|
contentStream.addRect(form.getX(), form.getY(), form.getWidth(), form.getHeight());
|
||||||
|
contentStream.clip();
|
||||||
|
|
||||||
|
// Draw the entire formXObject
|
||||||
|
contentStream.drawForm(formXObject);
|
||||||
|
|
||||||
|
contentStream.restoreGraphicsState();
|
||||||
|
|
||||||
|
contentStream.close();
|
||||||
|
|
||||||
|
// Now, set the new page's media box to the cropped size
|
||||||
|
newPage.setMediaBox(
|
||||||
|
new PDRectangle(form.getX(), form.getY(), form.getWidth(), form.getHeight()));
|
||||||
|
}
|
||||||
|
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
newDocument.save(baos);
|
||||||
|
newDocument.close();
|
||||||
|
sourceDocument.close();
|
||||||
|
|
||||||
|
byte[] pdfContent = baos.toByteArray();
|
||||||
|
return WebResponseUtils.bytesToWebResponse(
|
||||||
|
pdfContent,
|
||||||
|
form.getFileInput().getOriginalFilename().replaceFirst("[.][^.]+$", "")
|
||||||
|
+ "_cropped.pdf");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,78 +1,132 @@
|
|||||||
package stirling.software.SPDF.controller.api;
|
package stirling.software.SPDF.controller.api;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.nio.file.attribute.BasicFileAttributes;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.apache.pdfbox.io.MemoryUsageSetting;
|
||||||
|
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.apache.pdfbox.pdmodel.PDPageTree;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
|
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.RequestPart;
|
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.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 stirling.software.SPDF.utils.PdfUtils;
|
|
||||||
|
import stirling.software.SPDF.model.api.general.MergePdfsRequest;
|
||||||
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/general")
|
||||||
|
@Tag(name = "General", description = "General APIs")
|
||||||
public class MergeController {
|
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 {
|
||||||
// Create a new empty document
|
|
||||||
PDDocument mergedDoc = new PDDocument();
|
PDDocument mergedDoc = new PDDocument();
|
||||||
|
|
||||||
// Iterate over the list of documents and add their pages to the merged document
|
|
||||||
for (PDDocument doc : documents) {
|
for (PDDocument doc : documents) {
|
||||||
// Get all pages from the current document
|
for (PDPage page : doc.getPages()) {
|
||||||
PDPageTree pages = doc.getPages();
|
|
||||||
// Iterate over the pages and add them to the merged document
|
|
||||||
for (PDPage page : pages) {
|
|
||||||
mergedDoc.addPage(page);
|
mergedDoc.addPage(page);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the merged document
|
|
||||||
return mergedDoc;
|
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")
|
@PostMapping(consumes = "multipart/form-data", value = "/merge-pdfs")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Merge multiple PDF files into one",
|
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."
|
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(
|
public ResponseEntity<byte[]> mergePdfs(@ModelAttribute MergePdfsRequest form)
|
||||||
@RequestPart(required = true, value = "fileInput")
|
throws IOException {
|
||||||
@Parameter(description = "The input PDF files to be merged into a single file", required = true)
|
try {
|
||||||
MultipartFile[] files) throws IOException {
|
MultipartFile[] files = form.getFileInput();
|
||||||
// Read the input PDF files into PDDocument objects
|
Arrays.sort(files, getSortComparator(form.getSortType()));
|
||||||
List<PDDocument> documents = new ArrayList<>();
|
|
||||||
|
|
||||||
// Loop through the files array and read each file into a PDDocument
|
PDFMergerUtility mergedDoc = new PDFMergerUtility();
|
||||||
for (MultipartFile file : files) {
|
ByteArrayOutputStream docOutputstream = new ByteArrayOutputStream();
|
||||||
documents.add(PDDocument.load(file.getInputStream()));
|
|
||||||
|
for (MultipartFile file : files) {
|
||||||
|
mergedDoc.addSource(new ByteArrayInputStream(file.getBytes()));
|
||||||
|
}
|
||||||
|
|
||||||
|
mergedDoc.setDestinationFileName(
|
||||||
|
files[0].getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_merged.pdf");
|
||||||
|
mergedDoc.setDestinationStream(docOutputstream);
|
||||||
|
mergedDoc.mergeDocuments(MemoryUsageSetting.setupMainMemoryOnly());
|
||||||
|
|
||||||
|
return WebResponseUtils.bytesToWebResponse(
|
||||||
|
docOutputstream.toByteArray(), mergedDoc.getDestinationFileName());
|
||||||
|
} catch (Exception ex) {
|
||||||
|
logger.error("Error in merge pdf process", ex);
|
||||||
|
throw ex;
|
||||||
}
|
}
|
||||||
|
|
||||||
PDDocument mergedDoc = mergeDocuments(documents);
|
|
||||||
|
|
||||||
|
|
||||||
// Return the merged PDF as a response
|
|
||||||
ResponseEntity<byte[]> response = PdfUtils.pdfDocToWebResponse(mergedDoc, files[0].getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_merged.pdf");
|
|
||||||
|
|
||||||
for (PDDocument doc : documents) {
|
|
||||||
// Close the document after processing
|
|
||||||
doc.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
return response;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
package stirling.software.SPDF.controller.api;
|
||||||
|
|
||||||
|
import java.awt.Color;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import org.apache.pdfbox.multipdf.LayerUtility;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||||
|
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||||
|
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
|
||||||
|
import org.apache.pdfbox.util.Matrix;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.model.api.general.MergeMultiplePagesRequest;
|
||||||
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/general")
|
||||||
|
@Tag(name = "General", description = "General APIs")
|
||||||
|
public class MultiPageLayoutController {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(MultiPageLayoutController.class);
|
||||||
|
|
||||||
|
@PostMapping(value = "/multi-page-layout", consumes = "multipart/form-data")
|
||||||
|
@Operation(
|
||||||
|
summary = "Merge multiple pages of a PDF document into a single page",
|
||||||
|
description =
|
||||||
|
"This operation takes an input PDF file and the number of pages to merge into a single sheet in the output PDF file. Input:PDF Output:PDF Type:SISO")
|
||||||
|
public ResponseEntity<byte[]> mergeMultiplePagesIntoOne(
|
||||||
|
@ModelAttribute MergeMultiplePagesRequest request) throws IOException {
|
||||||
|
|
||||||
|
int pagesPerSheet = request.getPagesPerSheet();
|
||||||
|
MultipartFile file = request.getFileInput();
|
||||||
|
boolean addBorder = request.isAddBorder();
|
||||||
|
|
||||||
|
if (pagesPerSheet != 2
|
||||||
|
&& pagesPerSheet != 3
|
||||||
|
&& pagesPerSheet != (int) Math.sqrt(pagesPerSheet) * Math.sqrt(pagesPerSheet)) {
|
||||||
|
throw new IllegalArgumentException("pagesPerSheet must be 2, 3 or a perfect square");
|
||||||
|
}
|
||||||
|
|
||||||
|
int cols =
|
||||||
|
pagesPerSheet == 2 || pagesPerSheet == 3
|
||||||
|
? pagesPerSheet
|
||||||
|
: (int) Math.sqrt(pagesPerSheet);
|
||||||
|
int rows = pagesPerSheet == 2 || pagesPerSheet == 3 ? 1 : (int) Math.sqrt(pagesPerSheet);
|
||||||
|
|
||||||
|
PDDocument sourceDocument = PDDocument.load(file.getInputStream());
|
||||||
|
PDDocument newDocument = new PDDocument();
|
||||||
|
PDPage newPage = new PDPage(PDRectangle.A4);
|
||||||
|
newDocument.addPage(newPage);
|
||||||
|
|
||||||
|
int totalPages = sourceDocument.getNumberOfPages();
|
||||||
|
float cellWidth = newPage.getMediaBox().getWidth() / cols;
|
||||||
|
float cellHeight = newPage.getMediaBox().getHeight() / rows;
|
||||||
|
|
||||||
|
PDPageContentStream contentStream =
|
||||||
|
new PDPageContentStream(
|
||||||
|
newDocument, newPage, PDPageContentStream.AppendMode.APPEND, true, true);
|
||||||
|
LayerUtility layerUtility = new LayerUtility(newDocument);
|
||||||
|
|
||||||
|
float borderThickness = 1.5f; // Specify border thickness as required
|
||||||
|
contentStream.setLineWidth(borderThickness);
|
||||||
|
contentStream.setStrokingColor(Color.BLACK);
|
||||||
|
|
||||||
|
for (int i = 0; i < totalPages; i++) {
|
||||||
|
if (i != 0 && i % pagesPerSheet == 0) {
|
||||||
|
// Close the current content stream and create a new page and content stream
|
||||||
|
contentStream.close();
|
||||||
|
newPage = new PDPage(PDRectangle.A4);
|
||||||
|
newDocument.addPage(newPage);
|
||||||
|
contentStream =
|
||||||
|
new PDPageContentStream(
|
||||||
|
newDocument,
|
||||||
|
newPage,
|
||||||
|
PDPageContentStream.AppendMode.APPEND,
|
||||||
|
true,
|
||||||
|
true);
|
||||||
|
}
|
||||||
|
|
||||||
|
PDPage sourcePage = sourceDocument.getPage(i);
|
||||||
|
PDRectangle rect = sourcePage.getMediaBox();
|
||||||
|
float scaleWidth = cellWidth / rect.getWidth();
|
||||||
|
float scaleHeight = cellHeight / rect.getHeight();
|
||||||
|
float scale = Math.min(scaleWidth, scaleHeight);
|
||||||
|
|
||||||
|
int adjustedPageIndex =
|
||||||
|
i % pagesPerSheet; // This will reset the index for every new page
|
||||||
|
int rowIndex = adjustedPageIndex / cols;
|
||||||
|
int colIndex = adjustedPageIndex % cols;
|
||||||
|
|
||||||
|
float x = colIndex * cellWidth + (cellWidth - rect.getWidth() * scale) / 2;
|
||||||
|
float y =
|
||||||
|
newPage.getMediaBox().getHeight()
|
||||||
|
- ((rowIndex + 1) * cellHeight
|
||||||
|
- (cellHeight - rect.getHeight() * scale) / 2);
|
||||||
|
|
||||||
|
contentStream.saveGraphicsState();
|
||||||
|
contentStream.transform(Matrix.getTranslateInstance(x, y));
|
||||||
|
contentStream.transform(Matrix.getScaleInstance(scale, scale));
|
||||||
|
|
||||||
|
PDFormXObject formXObject = layerUtility.importPageAsForm(sourceDocument, i);
|
||||||
|
contentStream.drawForm(formXObject);
|
||||||
|
|
||||||
|
contentStream.restoreGraphicsState();
|
||||||
|
|
||||||
|
if (addBorder) {
|
||||||
|
// Draw border around each page
|
||||||
|
float borderX = colIndex * cellWidth;
|
||||||
|
float borderY = newPage.getMediaBox().getHeight() - (rowIndex + 1) * cellHeight;
|
||||||
|
contentStream.addRect(borderX, borderY, cellWidth, cellHeight);
|
||||||
|
contentStream.stroke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
contentStream.close(); // Close the final content stream
|
||||||
|
sourceDocument.close();
|
||||||
|
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
newDocument.save(baos);
|
||||||
|
newDocument.close();
|
||||||
|
|
||||||
|
byte[] result = baos.toByteArray();
|
||||||
|
return WebResponseUtils.bytesToWebResponse(
|
||||||
|
result,
|
||||||
|
file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_layoutChanged.pdf");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
package stirling.software.SPDF.controller.api;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.apache.pdfbox.multipdf.Overlay;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.model.api.general.OverlayPdfsRequest;
|
||||||
|
import stirling.software.SPDF.utils.GeneralUtils;
|
||||||
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/general")
|
||||||
|
@Tag(name = "General", description = "General APIs")
|
||||||
|
public class PdfOverlayController {
|
||||||
|
|
||||||
|
@PostMapping(value = "/overlay-pdfs", consumes = "multipart/form-data")
|
||||||
|
@Operation(
|
||||||
|
summary = "Overlay PDF files in various modes",
|
||||||
|
description =
|
||||||
|
"Overlay PDF files onto a base PDF with different modes: Sequential, Interleaved, or Fixed Repeat. Input:PDF Output:PDF Type:MIMO")
|
||||||
|
public ResponseEntity<byte[]> overlayPdfs(@ModelAttribute OverlayPdfsRequest request)
|
||||||
|
throws IOException {
|
||||||
|
MultipartFile baseFile = request.getFileInput();
|
||||||
|
int overlayPos = request.getOverlayPosition();
|
||||||
|
|
||||||
|
MultipartFile[] overlayFiles = request.getOverlayFiles();
|
||||||
|
File[] overlayPdfFiles = new File[overlayFiles.length];
|
||||||
|
List<File> tempFiles = new ArrayList<>(); // List to keep track of temporary files
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (int i = 0; i < overlayFiles.length; i++) {
|
||||||
|
overlayPdfFiles[i] = GeneralUtils.multipartToFile(overlayFiles[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
String mode = request.getOverlayMode(); // "SequentialOverlay", "InterleavedOverlay",
|
||||||
|
// "FixedRepeatOverlay"
|
||||||
|
int[] counts = request.getCounts(); // Used for FixedRepeatOverlay mode
|
||||||
|
|
||||||
|
try (PDDocument basePdf = PDDocument.load(baseFile.getInputStream());
|
||||||
|
Overlay overlay = new Overlay()) {
|
||||||
|
Map<Integer, String> overlayGuide =
|
||||||
|
prepareOverlayGuide(
|
||||||
|
basePdf.getNumberOfPages(),
|
||||||
|
overlayPdfFiles,
|
||||||
|
mode,
|
||||||
|
counts,
|
||||||
|
tempFiles);
|
||||||
|
|
||||||
|
overlay.setInputPDF(basePdf);
|
||||||
|
if (overlayPos == 0) {
|
||||||
|
overlay.setOverlayPosition(Overlay.Position.FOREGROUND);
|
||||||
|
} else {
|
||||||
|
overlay.setOverlayPosition(Overlay.Position.BACKGROUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||||
|
overlay.overlay(overlayGuide).save(outputStream);
|
||||||
|
byte[] data = outputStream.toByteArray();
|
||||||
|
String outputFilename =
|
||||||
|
baseFile.getOriginalFilename().replaceFirst("[.][^.]+$", "")
|
||||||
|
+ "_overlayed.pdf"; // Remove file extension and append .pdf
|
||||||
|
|
||||||
|
return WebResponseUtils.bytesToWebResponse(
|
||||||
|
data, outputFilename, MediaType.APPLICATION_PDF);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
for (File overlayPdfFile : overlayPdfFiles) {
|
||||||
|
if (overlayPdfFile != null) {
|
||||||
|
overlayPdfFile.delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (File tempFile : tempFiles) { // Delete temporary files
|
||||||
|
if (tempFile != null) {
|
||||||
|
tempFile.delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<Integer, String> prepareOverlayGuide(
|
||||||
|
int basePageCount, File[] overlayFiles, String mode, int[] counts, List<File> tempFiles)
|
||||||
|
throws IOException {
|
||||||
|
Map<Integer, String> overlayGuide = new HashMap<>();
|
||||||
|
switch (mode) {
|
||||||
|
case "SequentialOverlay":
|
||||||
|
sequentialOverlay(overlayGuide, overlayFiles, basePageCount, tempFiles);
|
||||||
|
break;
|
||||||
|
case "InterleavedOverlay":
|
||||||
|
interleavedOverlay(overlayGuide, overlayFiles, basePageCount);
|
||||||
|
break;
|
||||||
|
case "FixedRepeatOverlay":
|
||||||
|
fixedRepeatOverlay(overlayGuide, overlayFiles, counts, basePageCount);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException("Invalid overlay mode");
|
||||||
|
}
|
||||||
|
return overlayGuide;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sequentialOverlay(
|
||||||
|
Map<Integer, String> overlayGuide,
|
||||||
|
File[] overlayFiles,
|
||||||
|
int basePageCount,
|
||||||
|
List<File> tempFiles)
|
||||||
|
throws IOException {
|
||||||
|
int overlayFileIndex = 0;
|
||||||
|
int pageCountInCurrentOverlay = 0;
|
||||||
|
|
||||||
|
for (int basePageIndex = 1; basePageIndex <= basePageCount; basePageIndex++) {
|
||||||
|
if (pageCountInCurrentOverlay == 0
|
||||||
|
|| pageCountInCurrentOverlay
|
||||||
|
>= getNumberOfPages(overlayFiles[overlayFileIndex])) {
|
||||||
|
pageCountInCurrentOverlay = 0;
|
||||||
|
overlayFileIndex = (overlayFileIndex + 1) % overlayFiles.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
try (PDDocument overlayPdf = PDDocument.load(overlayFiles[overlayFileIndex])) {
|
||||||
|
PDDocument singlePageDocument = new PDDocument();
|
||||||
|
singlePageDocument.addPage(overlayPdf.getPage(pageCountInCurrentOverlay));
|
||||||
|
File tempFile = File.createTempFile("overlay-page-", ".pdf");
|
||||||
|
singlePageDocument.save(tempFile);
|
||||||
|
singlePageDocument.close();
|
||||||
|
|
||||||
|
overlayGuide.put(basePageIndex, tempFile.getAbsolutePath());
|
||||||
|
tempFiles.add(tempFile); // Keep track of the temporary file for cleanup
|
||||||
|
}
|
||||||
|
|
||||||
|
pageCountInCurrentOverlay++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getNumberOfPages(File file) throws IOException {
|
||||||
|
try (PDDocument doc = PDDocument.load(file)) {
|
||||||
|
return doc.getNumberOfPages();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void interleavedOverlay(
|
||||||
|
Map<Integer, String> overlayGuide, File[] overlayFiles, int basePageCount)
|
||||||
|
throws IOException {
|
||||||
|
for (int basePageIndex = 1; basePageIndex <= basePageCount; basePageIndex++) {
|
||||||
|
File overlayFile = overlayFiles[(basePageIndex - 1) % overlayFiles.length];
|
||||||
|
|
||||||
|
// Load the overlay document to check its page count
|
||||||
|
try (PDDocument overlayPdf = PDDocument.load(overlayFile)) {
|
||||||
|
int overlayPageCount = overlayPdf.getNumberOfPages();
|
||||||
|
if ((basePageIndex - 1) % overlayPageCount < overlayPageCount) {
|
||||||
|
overlayGuide.put(basePageIndex, overlayFile.getAbsolutePath());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fixedRepeatOverlay(
|
||||||
|
Map<Integer, String> overlayGuide, File[] overlayFiles, int[] counts, int basePageCount)
|
||||||
|
throws IOException {
|
||||||
|
if (overlayFiles.length != counts.length) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Counts array length must match the number of overlay files");
|
||||||
|
}
|
||||||
|
int currentPage = 1;
|
||||||
|
for (int i = 0; i < overlayFiles.length; i++) {
|
||||||
|
File overlayFile = overlayFiles[i];
|
||||||
|
int repeatCount = counts[i];
|
||||||
|
|
||||||
|
// Load the overlay document to check its page count
|
||||||
|
try (PDDocument overlayPdf = PDDocument.load(overlayFile)) {
|
||||||
|
int overlayPageCount = overlayPdf.getNumberOfPages();
|
||||||
|
for (int j = 0; j < repeatCount; j++) {
|
||||||
|
for (int page = 0; page < overlayPageCount; page++) {
|
||||||
|
if (currentPage > basePageCount) break;
|
||||||
|
overlayGuide.put(currentPage++, overlayFile.getAbsolutePath());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Additional classes like OverlayPdfsRequest, WebResponseUtils, etc. are assumed to be defined
|
||||||
|
// elsewhere.
|
||||||
@@ -9,97 +9,189 @@ import org.apache.pdfbox.pdmodel.PDPage;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
|
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.RequestParam;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
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.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 stirling.software.SPDF.utils.PdfUtils;
|
|
||||||
|
import stirling.software.SPDF.model.SortTypes;
|
||||||
|
import stirling.software.SPDF.model.api.PDFWithPageNums;
|
||||||
|
import stirling.software.SPDF.model.api.general.RearrangePagesRequest;
|
||||||
|
import stirling.software.SPDF.utils.GeneralUtils;
|
||||||
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/general")
|
||||||
|
@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",
|
@Operation(
|
||||||
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.")
|
summary = "Remove pages from a PDF file",
|
||||||
public ResponseEntity<byte[]> deletePages(
|
description =
|
||||||
@RequestPart(required = true, value = "fileInput")
|
"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")
|
||||||
@Parameter(description = "The input PDF file from which pages will be removed")
|
public ResponseEntity<byte[]> deletePages(@ModelAttribute PDFWithPageNums request)
|
||||||
MultipartFile pdfFile,
|
throws IOException {
|
||||||
@RequestParam("pagesToDelete")
|
|
||||||
@Parameter(description = "Comma-separated list of pages or page ranges to delete, e.g., '1,3,5-8'")
|
MultipartFile pdfFile = request.getFileInput();
|
||||||
String pagesToDelete) throws IOException {
|
String pagesToDelete = request.getPageNumbers();
|
||||||
|
|
||||||
PDDocument document = PDDocument.load(pdfFile.getBytes());
|
PDDocument document = PDDocument.load(pdfFile.getBytes());
|
||||||
|
|
||||||
// Split the page order string into an array of page numbers or range of numbers
|
// Split the page order string into an array of page numbers or range of numbers
|
||||||
String[] pageOrderArr = pagesToDelete.split(",");
|
String[] pageOrderArr = pagesToDelete.split(",");
|
||||||
|
|
||||||
List<Integer> pagesToRemove = pageOrderToString(pageOrderArr, document.getNumberOfPages());
|
List<Integer> pagesToRemove =
|
||||||
|
GeneralUtils.parsePageList(pageOrderArr, document.getNumberOfPages());
|
||||||
|
|
||||||
for (int i = pagesToRemove.size() - 1; i >= 0; i--) {
|
for (int i = pagesToRemove.size() - 1; i >= 0; i--) {
|
||||||
int pageIndex = pagesToRemove.get(i);
|
int pageIndex = pagesToRemove.get(i);
|
||||||
document.removePage(pageIndex);
|
document.removePage(pageIndex);
|
||||||
}
|
}
|
||||||
return PdfUtils.pdfDocToWebResponse(document, pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_removed_pages.pdf");
|
return WebResponseUtils.pdfDocToWebResponse(
|
||||||
|
document,
|
||||||
|
pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_removed_pages.pdf");
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<Integer> pageOrderToString(String[] pageOrderArr, int totalPages) {
|
private List<Integer> removeFirst(int totalPages) {
|
||||||
|
if (totalPages <= 1) return new ArrayList<>();
|
||||||
List<Integer> newPageOrder = new ArrayList<>();
|
List<Integer> newPageOrder = new ArrayList<>();
|
||||||
// loop through the page order array
|
for (int i = 2; i <= totalPages; i++) {
|
||||||
for (String element : pageOrderArr) {
|
newPageOrder.add(i - 1);
|
||||||
// check if the element contains a range of pages
|
|
||||||
if (element.contains("-")) {
|
|
||||||
// split the range into start and end page
|
|
||||||
String[] range = element.split("-");
|
|
||||||
int start = Integer.parseInt(range[0]);
|
|
||||||
int end = Integer.parseInt(range[1]);
|
|
||||||
// check if the end page is greater than total pages
|
|
||||||
if (end > totalPages) {
|
|
||||||
end = totalPages;
|
|
||||||
}
|
|
||||||
// loop through the range of pages
|
|
||||||
for (int j = start; j <= end; j++) {
|
|
||||||
// print the current index
|
|
||||||
newPageOrder.add(j - 1);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// if the element is a single page
|
|
||||||
newPageOrder.add(Integer.parseInt(element) - 1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return newPageOrder;
|
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) {
|
||||||
|
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")
|
@PostMapping(consumes = "multipart/form-data", value = "/rearrange-pages")
|
||||||
@Operation(summary = "Rearrange pages in a PDF file",
|
@Operation(
|
||||||
description = "This endpoint rearranges pages in a given PDF file based on the specified page order. Users can provide a page order as a comma-separated list of page numbers or page ranges.")
|
summary = "Rearrange pages in a PDF file",
|
||||||
public ResponseEntity<byte[]> rearrangePages(
|
description =
|
||||||
@RequestPart(required = true, value = "fileInput")
|
"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")
|
||||||
@Parameter(description = "The input PDF file to rearrange pages")
|
public ResponseEntity<byte[]> rearrangePages(@ModelAttribute RearrangePagesRequest request)
|
||||||
MultipartFile pdfFile,
|
throws IOException {
|
||||||
@RequestParam("pageOrder")
|
MultipartFile pdfFile = request.getFileInput();
|
||||||
@Parameter(description = "The new page order as a comma-separated list of page numbers or page ranges (e.g., '1,3,5-7')")
|
String pageOrder = request.getPageNumbers();
|
||||||
String pageOrder) {
|
String sortType = request.getCustomMode();
|
||||||
try {
|
try {
|
||||||
// Load the input PDF
|
// Load the input PDF
|
||||||
PDDocument document = PDDocument.load(pdfFile.getInputStream());
|
PDDocument document = PDDocument.load(pdfFile.getInputStream());
|
||||||
|
|
||||||
// Split the page order string into an array of page numbers or range of numbers
|
// Split the page order string into an array of page numbers or range of numbers
|
||||||
String[] pageOrderArr = pageOrder.split(",");
|
String[] pageOrderArr = pageOrder != null ? pageOrder.split(",") : new String[0];
|
||||||
// int[] newPageOrder = new int[pageOrderArr.length];
|
|
||||||
int totalPages = document.getNumberOfPages();
|
int totalPages = document.getNumberOfPages();
|
||||||
|
List<Integer> newPageOrder;
|
||||||
List<Integer> newPageOrder = pageOrderToString(pageOrderArr, totalPages);
|
if (sortType != null && sortType.length() > 0) {
|
||||||
|
newPageOrder = processSortTypes(sortType, totalPages);
|
||||||
|
} else {
|
||||||
|
newPageOrder = GeneralUtils.parsePageList(pageOrderArr, totalPages);
|
||||||
|
}
|
||||||
|
logger.info("newPageOrder = " + newPageOrder);
|
||||||
|
logger.info("totalPages = " + totalPages);
|
||||||
// Create a new list to hold the pages in the new order
|
// Create a new list to hold the pages in the new order
|
||||||
List<PDPage> newPages = new ArrayList<>();
|
List<PDPage> newPages = new ArrayList<>();
|
||||||
for (int i = 0; i < newPageOrder.size(); i++) {
|
for (int i = 0; i < newPageOrder.size(); i++) {
|
||||||
@@ -116,12 +208,13 @@ public class RearrangePagesPDFController {
|
|||||||
document.addPage(page);
|
document.addPage(page);
|
||||||
}
|
}
|
||||||
|
|
||||||
return PdfUtils.pdfDocToWebResponse(document, pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_rearranged.pdf");
|
return WebResponseUtils.pdfDocToWebResponse(
|
||||||
|
document,
|
||||||
|
pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "")
|
||||||
|
+ "_rearranged.pdf");
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
|
|
||||||
logger.error("Failed rearranging documents", e);
|
logger.error("Failed rearranging documents", e);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,34 +8,34 @@ import org.apache.pdfbox.pdmodel.PDPageTree;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
|
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.RequestParam;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
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.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 stirling.software.SPDF.utils.PdfUtils;
|
|
||||||
|
import stirling.software.SPDF.model.api.general.RotatePDFRequest;
|
||||||
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/general")
|
||||||
|
@Tag(name = "General", description = "General APIs")
|
||||||
public class RotationController {
|
public class RotationController {
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(RotationController.class);
|
private static final Logger logger = LoggerFactory.getLogger(RotationController.class);
|
||||||
|
|
||||||
@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."
|
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)
|
||||||
@RequestPart(required = true, value = "fileInput")
|
throws IOException {
|
||||||
@Parameter(description = "The PDF file to be rotated", required = true)
|
MultipartFile pdfFile = request.getFileInput();
|
||||||
MultipartFile pdfFile,
|
Integer angle = request.getAngle();
|
||||||
@RequestParam("angle")
|
|
||||||
@Parameter(description = "The angle by which to rotate the PDF file. This should be a multiple of 90.", example = "90", required = true)
|
|
||||||
Integer angle) throws IOException {
|
|
||||||
|
|
||||||
// Load the PDF document
|
// Load the PDF document
|
||||||
PDDocument document = PDDocument.load(pdfFile.getBytes());
|
PDDocument document = PDDocument.load(pdfFile.getBytes());
|
||||||
|
|
||||||
@@ -46,8 +46,8 @@ public class RotationController {
|
|||||||
page.setRotation(page.getRotation() + angle);
|
page.setRotation(page.getRotation() + angle);
|
||||||
}
|
}
|
||||||
|
|
||||||
return PdfUtils.pdfDocToWebResponse(document, pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_rotated.pdf");
|
return WebResponseUtils.pdfDocToWebResponse(
|
||||||
|
document,
|
||||||
|
pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_rotated.pdf");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
package stirling.software.SPDF.controller.api;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.apache.pdfbox.multipdf.LayerUtility;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||||
|
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||||
|
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
|
||||||
|
import org.apache.pdfbox.util.Matrix;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.model.api.general.ScalePagesRequest;
|
||||||
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/general")
|
||||||
|
@Tag(name = "General", description = "General APIs")
|
||||||
|
public class ScalePagesController {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(ScalePagesController.class);
|
||||||
|
|
||||||
|
@PostMapping(value = "/scale-pages", consumes = "multipart/form-data")
|
||||||
|
@Operation(
|
||||||
|
summary = "Change the size of a PDF page/document",
|
||||||
|
description =
|
||||||
|
"This operation takes an input PDF file and the size to scale the pages to in the output PDF file. Input:PDF Output:PDF Type:SISO")
|
||||||
|
public ResponseEntity<byte[]> scalePages(@ModelAttribute ScalePagesRequest request)
|
||||||
|
throws IOException {
|
||||||
|
MultipartFile file = request.getFileInput();
|
||||||
|
String targetPDRectangle = request.getPageSize();
|
||||||
|
float scaleFactor = request.getScaleFactor();
|
||||||
|
|
||||||
|
Map<String, PDRectangle> sizeMap = new HashMap<>();
|
||||||
|
// Add A0 - A10
|
||||||
|
sizeMap.put("A0", PDRectangle.A0);
|
||||||
|
sizeMap.put("A1", PDRectangle.A1);
|
||||||
|
sizeMap.put("A2", PDRectangle.A2);
|
||||||
|
sizeMap.put("A3", PDRectangle.A3);
|
||||||
|
sizeMap.put("A4", PDRectangle.A4);
|
||||||
|
sizeMap.put("A5", PDRectangle.A5);
|
||||||
|
sizeMap.put("A6", PDRectangle.A6);
|
||||||
|
|
||||||
|
// Add other sizes
|
||||||
|
sizeMap.put("LETTER", PDRectangle.LETTER);
|
||||||
|
sizeMap.put("LEGAL", PDRectangle.LEGAL);
|
||||||
|
|
||||||
|
if (!sizeMap.containsKey(targetPDRectangle)) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"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);
|
||||||
|
|
||||||
|
PDDocument sourceDocument = PDDocument.load(file.getBytes());
|
||||||
|
PDDocument outputDocument = new PDDocument();
|
||||||
|
|
||||||
|
int totalPages = sourceDocument.getNumberOfPages();
|
||||||
|
for (int i = 0; i < totalPages; i++) {
|
||||||
|
PDPage sourcePage = sourceDocument.getPage(i);
|
||||||
|
PDRectangle sourceSize = sourcePage.getMediaBox();
|
||||||
|
|
||||||
|
float scaleWidth = targetSize.getWidth() / sourceSize.getWidth();
|
||||||
|
float scaleHeight = targetSize.getHeight() / sourceSize.getHeight();
|
||||||
|
float scale = Math.min(scaleWidth, scaleHeight) * scaleFactor;
|
||||||
|
|
||||||
|
PDPage newPage = new PDPage(targetSize);
|
||||||
|
outputDocument.addPage(newPage);
|
||||||
|
|
||||||
|
PDPageContentStream contentStream =
|
||||||
|
new PDPageContentStream(
|
||||||
|
outputDocument, newPage, PDPageContentStream.AppendMode.APPEND, true);
|
||||||
|
|
||||||
|
float x = (targetSize.getWidth() - sourceSize.getWidth() * scale) / 2;
|
||||||
|
float y = (targetSize.getHeight() - sourceSize.getHeight() * scale) / 2;
|
||||||
|
|
||||||
|
contentStream.saveGraphicsState();
|
||||||
|
contentStream.transform(Matrix.getTranslateInstance(x, y));
|
||||||
|
contentStream.transform(Matrix.getScaleInstance(scale, scale));
|
||||||
|
|
||||||
|
LayerUtility layerUtility = new LayerUtility(outputDocument);
|
||||||
|
PDFormXObject form = layerUtility.importPageAsForm(sourceDocument, i);
|
||||||
|
contentStream.drawForm(form);
|
||||||
|
|
||||||
|
contentStream.restoreGraphicsState();
|
||||||
|
contentStream.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
outputDocument.save(baos);
|
||||||
|
outputDocument.close();
|
||||||
|
sourceDocument.close();
|
||||||
|
|
||||||
|
return WebResponseUtils.bytesToWebResponse(
|
||||||
|
baos.toByteArray(),
|
||||||
|
file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_scaled.pdf");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,6 @@ 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;
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipEntry;
|
||||||
@@ -16,81 +15,58 @@ 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;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.core.io.ByteArrayResource;
|
|
||||||
import org.springframework.core.io.Resource;
|
|
||||||
import org.springframework.http.HttpHeaders;
|
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.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.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 stirling.software.SPDF.model.api.PDFWithPageNums;
|
||||||
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/general")
|
||||||
|
@Tag(name = "General", description = "General APIs")
|
||||||
public class SplitPDFController {
|
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.")
|
summary = "Split a PDF file into separate documents",
|
||||||
public ResponseEntity<Resource> splitPdf(
|
description =
|
||||||
@RequestPart(required = true, value = "fileInput")
|
"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")
|
||||||
@Parameter(description = "The input PDF file to be split")
|
public ResponseEntity<byte[]> splitPdf(@ModelAttribute PDFWithPageNums request)
|
||||||
MultipartFile file,
|
throws IOException {
|
||||||
@RequestParam("pages")
|
MultipartFile file = request.getFileInput();
|
||||||
@Parameter(description = "The pages to be included in separate documents. Specify individual page numbers (e.g., '1,3,5'), ranges (e.g., '1-3,5-7'), or 'all' for every page.")
|
String pages = request.getPageNumbers();
|
||||||
String pages) throws IOException {
|
|
||||||
// parse user input
|
|
||||||
|
|
||||||
// open the pdf document
|
// open the pdf document
|
||||||
InputStream inputStream = file.getInputStream();
|
InputStream inputStream = file.getInputStream();
|
||||||
PDDocument document = PDDocument.load(inputStream);
|
PDDocument document = PDDocument.load(inputStream);
|
||||||
|
|
||||||
List<Integer> pageNumbers = new ArrayList<>();
|
List<Integer> pageNumbers = request.getPageNumbersList(document);
|
||||||
pages = pages.replaceAll("\\s+", ""); // remove whitespaces
|
if (!pageNumbers.contains(document.getNumberOfPages() - 1))
|
||||||
if (pages.toLowerCase().equals("all")) {
|
pageNumbers.add(document.getNumberOfPages() - 1);
|
||||||
for (int i = 0; i < document.getNumberOfPages(); i++) {
|
logger.info(
|
||||||
pageNumbers.add(i);
|
"Splitting PDF into pages: {}",
|
||||||
}
|
pageNumbers.stream().map(String::valueOf).collect(Collectors.joining(",")));
|
||||||
} else {
|
|
||||||
List<String> pageNumbersStr = new ArrayList<>(Arrays.asList(pages.split(",")));
|
|
||||||
if (!pageNumbersStr.contains(String.valueOf(document.getNumberOfPages()))) {
|
|
||||||
String lastpage = String.valueOf(document.getNumberOfPages());
|
|
||||||
pageNumbersStr.add(lastpage);
|
|
||||||
}
|
|
||||||
for (String page : pageNumbersStr) {
|
|
||||||
if (page.contains("-")) {
|
|
||||||
String[] range = page.split("-");
|
|
||||||
int start = Integer.parseInt(range[0]);
|
|
||||||
int end = Integer.parseInt(range[1]);
|
|
||||||
for (int i = start; i <= end; i++) {
|
|
||||||
pageNumbers.add(i);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
pageNumbers.add(Integer.parseInt(page));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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<>();
|
||||||
int currentPage = 0;
|
int previousPageNumber = 0;
|
||||||
for (int pageNumber : pageNumbers) {
|
for (int splitPoint : pageNumbers) {
|
||||||
try (PDDocument splitDocument = new PDDocument()) {
|
try (PDDocument splitDocument = new PDDocument()) {
|
||||||
for (int i = currentPage; i < pageNumber; i++) {
|
for (int i = previousPageNumber; i <= splitPoint; i++) {
|
||||||
PDPage page = document.getPage(i);
|
PDPage page = document.getPage(i);
|
||||||
splitDocument.addPage(page);
|
splitDocument.addPage(page);
|
||||||
logger.debug("Adding page {} to split document", i);
|
logger.debug("Adding page {} to split document", i);
|
||||||
}
|
}
|
||||||
currentPage = pageNumber;
|
previousPageNumber = splitPoint + 1;
|
||||||
logger.debug("Setting current page to {}", currentPage);
|
|
||||||
|
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
splitDocument.save(baos);
|
splitDocument.save(baos);
|
||||||
@@ -107,10 +83,11 @@ public class SplitPDFController {
|
|||||||
|
|
||||||
Path zipFile = Files.createTempFile("split_documents", ".zip");
|
Path zipFile = Files.createTempFile("split_documents", ".zip");
|
||||||
|
|
||||||
|
String filename = 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++) {
|
||||||
String fileName = "split_document_" + (i + 1) + ".pdf";
|
String fileName = filename + "_" + (i + 1) + ".pdf";
|
||||||
ByteArrayOutputStream baos = splitDocumentsBoas.get(i);
|
ByteArrayOutputStream baos = splitDocumentsBoas.get(i);
|
||||||
byte[] pdf = baos.toByteArray();
|
byte[] pdf = baos.toByteArray();
|
||||||
|
|
||||||
@@ -129,12 +106,10 @@ public class SplitPDFController {
|
|||||||
|
|
||||||
logger.info("Successfully created zip file with split documents: {}", zipFile.toString());
|
logger.info("Successfully created zip file with split documents: {}", zipFile.toString());
|
||||||
byte[] data = Files.readAllBytes(zipFile);
|
byte[] data = Files.readAllBytes(zipFile);
|
||||||
ByteArrayResource resource = new ByteArrayResource(data);
|
|
||||||
Files.delete(zipFile);
|
Files.delete(zipFile);
|
||||||
|
|
||||||
// return the Resource in the response
|
// return the Resource in the response
|
||||||
return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_split.zip")
|
return WebResponseUtils.bytesToWebResponse(
|
||||||
.contentType(MediaType.APPLICATION_OCTET_STREAM).contentLength(resource.contentLength()).body(resource);
|
data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
package stirling.software.SPDF.controller.api;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
import java.util.zip.ZipOutputStream;
|
||||||
|
|
||||||
|
import org.apache.pdfbox.multipdf.LayerUtility;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||||
|
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||||
|
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
|
||||||
|
import org.apache.pdfbox.util.Matrix;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.model.api.SplitPdfBySectionsRequest;
|
||||||
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/general")
|
||||||
|
@Tag(name = "General", description = "General APIs")
|
||||||
|
public class SplitPdfBySectionsController {
|
||||||
|
|
||||||
|
@PostMapping(value = "/split-pdf-by-sections", consumes = "multipart/form-data")
|
||||||
|
@Operation(
|
||||||
|
summary = "Split PDF pages into smaller sections",
|
||||||
|
description =
|
||||||
|
"Split each page of a PDF into smaller sections based on the user's choice (halves, thirds, quarters, etc.), both vertically and horizontally. Input:PDF Output:ZIP-PDF Type:SISO")
|
||||||
|
public ResponseEntity<byte[]> splitPdf(@ModelAttribute SplitPdfBySectionsRequest request)
|
||||||
|
throws Exception {
|
||||||
|
List<ByteArrayOutputStream> splitDocumentsBoas = new ArrayList<>();
|
||||||
|
|
||||||
|
MultipartFile file = request.getFileInput();
|
||||||
|
PDDocument sourceDocument = PDDocument.load(file.getInputStream());
|
||||||
|
|
||||||
|
// Process the PDF based on split parameters
|
||||||
|
int horiz = request.getHorizontalDivisions() + 1;
|
||||||
|
int verti = request.getVerticalDivisions() + 1;
|
||||||
|
|
||||||
|
List<PDDocument> splitDocuments = splitPdfPages(sourceDocument, verti, horiz);
|
||||||
|
for (PDDocument doc : splitDocuments) {
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
doc.save(baos);
|
||||||
|
doc.close();
|
||||||
|
splitDocumentsBoas.add(baos);
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceDocument.close();
|
||||||
|
|
||||||
|
Path zipFile = Files.createTempFile("split_documents", ".zip");
|
||||||
|
String filename = file.getOriginalFilename().replaceFirst("[.][^.]+$", "");
|
||||||
|
byte[] data;
|
||||||
|
|
||||||
|
try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(zipFile))) {
|
||||||
|
int pageNum = 1;
|
||||||
|
for (int i = 0; i < splitDocumentsBoas.size(); i++) {
|
||||||
|
ByteArrayOutputStream baos = splitDocumentsBoas.get(i);
|
||||||
|
int sectionNum = (i % (horiz * verti)) + 1;
|
||||||
|
String fileName = filename + "_" + pageNum + "_" + sectionNum + ".pdf";
|
||||||
|
byte[] pdf = baos.toByteArray();
|
||||||
|
ZipEntry pdfEntry = new ZipEntry(fileName);
|
||||||
|
zipOut.putNextEntry(pdfEntry);
|
||||||
|
zipOut.write(pdf);
|
||||||
|
zipOut.closeEntry();
|
||||||
|
|
||||||
|
if (sectionNum == horiz * verti) pageNum++;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
} finally {
|
||||||
|
data = Files.readAllBytes(zipFile);
|
||||||
|
Files.delete(zipFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
return WebResponseUtils.bytesToWebResponse(
|
||||||
|
data, filename + "_split.zip", MediaType.APPLICATION_OCTET_STREAM);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<PDDocument> splitPdfPages(
|
||||||
|
PDDocument document, int horizontalDivisions, int verticalDivisions)
|
||||||
|
throws IOException {
|
||||||
|
List<PDDocument> splitDocuments = new ArrayList<>();
|
||||||
|
|
||||||
|
for (PDPage originalPage : document.getPages()) {
|
||||||
|
PDRectangle originalMediaBox = originalPage.getMediaBox();
|
||||||
|
float width = originalMediaBox.getWidth();
|
||||||
|
float height = originalMediaBox.getHeight();
|
||||||
|
float subPageWidth = width / horizontalDivisions;
|
||||||
|
float subPageHeight = height / verticalDivisions;
|
||||||
|
|
||||||
|
LayerUtility layerUtility = new LayerUtility(document);
|
||||||
|
|
||||||
|
for (int i = 0; i < horizontalDivisions; i++) {
|
||||||
|
for (int j = 0; j < verticalDivisions; j++) {
|
||||||
|
PDDocument subDoc = new PDDocument();
|
||||||
|
PDPage subPage = new PDPage(new PDRectangle(subPageWidth, subPageHeight));
|
||||||
|
subDoc.addPage(subPage);
|
||||||
|
|
||||||
|
PDFormXObject form =
|
||||||
|
layerUtility.importPageAsForm(
|
||||||
|
document, document.getPages().indexOf(originalPage));
|
||||||
|
|
||||||
|
try (PDPageContentStream contentStream =
|
||||||
|
new PDPageContentStream(subDoc, subPage)) {
|
||||||
|
// Set clipping area and position
|
||||||
|
float translateX = -subPageWidth * i;
|
||||||
|
float translateY = height - subPageHeight * (verticalDivisions - j);
|
||||||
|
|
||||||
|
contentStream.saveGraphicsState();
|
||||||
|
contentStream.addRect(0, 0, subPageWidth, subPageHeight);
|
||||||
|
contentStream.clip();
|
||||||
|
contentStream.transform(new Matrix(1, 0, 0, 1, translateX, translateY));
|
||||||
|
|
||||||
|
// Draw the form
|
||||||
|
contentStream.drawForm(form);
|
||||||
|
contentStream.restoreGraphicsState();
|
||||||
|
}
|
||||||
|
|
||||||
|
splitDocuments.add(subDoc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return splitDocuments;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
package stirling.software.SPDF.controller.api;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
import java.util.zip.ZipOutputStream;
|
||||||
|
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.model.api.general.SplitPdfBySizeOrCountRequest;
|
||||||
|
import stirling.software.SPDF.utils.GeneralUtils;
|
||||||
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/general")
|
||||||
|
@Tag(name = "General", description = "General APIs")
|
||||||
|
public class SplitPdfBySizeController {
|
||||||
|
|
||||||
|
@PostMapping(value = "/split-by-size-or-count", consumes = "multipart/form-data")
|
||||||
|
@Operation(
|
||||||
|
summary = "Auto split PDF pages into separate documents based on size or count",
|
||||||
|
description =
|
||||||
|
"split PDF into multiple paged documents based on size/count, ie if 20 pages and split into 5, it does 5 documents each 4 pages\r\n"
|
||||||
|
+ " if 10MB and each page is 1MB and you enter 2MB then 5 docs each 2MB (rounded so that it accepts 1.9MB but not 2.1MB) Input:PDF Output:ZIP-PDF Type:SISO")
|
||||||
|
public ResponseEntity<byte[]> autoSplitPdf(@ModelAttribute SplitPdfBySizeOrCountRequest request)
|
||||||
|
throws Exception {
|
||||||
|
List<ByteArrayOutputStream> splitDocumentsBoas = new ArrayList<ByteArrayOutputStream>();
|
||||||
|
|
||||||
|
MultipartFile file = request.getFileInput();
|
||||||
|
PDDocument sourceDocument = PDDocument.load(file.getInputStream());
|
||||||
|
|
||||||
|
// 0 = size, 1 = page count, 2 = doc count
|
||||||
|
int type = request.getSplitType();
|
||||||
|
String value = request.getSplitValue();
|
||||||
|
|
||||||
|
if (type == 0) { // Split by size
|
||||||
|
long maxBytes = GeneralUtils.convertSizeToBytes(value);
|
||||||
|
long currentSize = 0;
|
||||||
|
PDDocument currentDoc = new PDDocument();
|
||||||
|
|
||||||
|
for (PDPage page : sourceDocument.getPages()) {
|
||||||
|
ByteArrayOutputStream pageOutputStream = new ByteArrayOutputStream();
|
||||||
|
PDDocument tempDoc = new PDDocument();
|
||||||
|
tempDoc.addPage(page);
|
||||||
|
tempDoc.save(pageOutputStream);
|
||||||
|
tempDoc.close();
|
||||||
|
|
||||||
|
long pageSize = pageOutputStream.size();
|
||||||
|
if (currentSize + pageSize > maxBytes) {
|
||||||
|
// Save and reset current document
|
||||||
|
splitDocumentsBoas.add(currentDocToByteArray(currentDoc));
|
||||||
|
currentDoc = new PDDocument();
|
||||||
|
currentSize = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentDoc.addPage(page);
|
||||||
|
currentSize += pageSize;
|
||||||
|
}
|
||||||
|
// Add the last document if it contains any pages
|
||||||
|
if (currentDoc.getPages().getCount() != 0) {
|
||||||
|
splitDocumentsBoas.add(currentDocToByteArray(currentDoc));
|
||||||
|
}
|
||||||
|
} else if (type == 1) { // Split by page count
|
||||||
|
int pageCount = Integer.parseInt(value);
|
||||||
|
int currentPageCount = 0;
|
||||||
|
PDDocument currentDoc = new PDDocument();
|
||||||
|
|
||||||
|
for (PDPage page : sourceDocument.getPages()) {
|
||||||
|
currentDoc.addPage(page);
|
||||||
|
currentPageCount++;
|
||||||
|
|
||||||
|
if (currentPageCount == pageCount) {
|
||||||
|
// Save and reset current document
|
||||||
|
splitDocumentsBoas.add(currentDocToByteArray(currentDoc));
|
||||||
|
currentDoc = new PDDocument();
|
||||||
|
currentPageCount = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Add the last document if it contains any pages
|
||||||
|
if (currentDoc.getPages().getCount() != 0) {
|
||||||
|
splitDocumentsBoas.add(currentDocToByteArray(currentDoc));
|
||||||
|
}
|
||||||
|
} else if (type == 2) { // Split by doc count
|
||||||
|
int documentCount = Integer.parseInt(value);
|
||||||
|
int totalPageCount = sourceDocument.getNumberOfPages();
|
||||||
|
int pagesPerDocument = totalPageCount / documentCount;
|
||||||
|
int extraPages = totalPageCount % documentCount;
|
||||||
|
int currentPageIndex = 0;
|
||||||
|
|
||||||
|
for (int i = 0; i < documentCount; i++) {
|
||||||
|
PDDocument currentDoc = new PDDocument();
|
||||||
|
int pagesToAdd = pagesPerDocument + (i < extraPages ? 1 : 0);
|
||||||
|
|
||||||
|
for (int j = 0; j < pagesToAdd; j++) {
|
||||||
|
currentDoc.addPage(sourceDocument.getPage(currentPageIndex++));
|
||||||
|
}
|
||||||
|
|
||||||
|
splitDocumentsBoas.add(currentDocToByteArray(currentDoc));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException("Invalid argument for split type");
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceDocument.close();
|
||||||
|
|
||||||
|
Path zipFile = Files.createTempFile("split_documents", ".zip");
|
||||||
|
String filename = file.getOriginalFilename().replaceFirst("[.][^.]+$", "");
|
||||||
|
byte[] data;
|
||||||
|
|
||||||
|
try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(zipFile))) {
|
||||||
|
for (int i = 0; i < splitDocumentsBoas.size(); i++) {
|
||||||
|
String fileName = filename + "_" + (i + 1) + ".pdf";
|
||||||
|
ByteArrayOutputStream baos = splitDocumentsBoas.get(i);
|
||||||
|
byte[] pdf = baos.toByteArray();
|
||||||
|
|
||||||
|
ZipEntry pdfEntry = new ZipEntry(fileName);
|
||||||
|
zipOut.putNextEntry(pdfEntry);
|
||||||
|
zipOut.write(pdf);
|
||||||
|
zipOut.closeEntry();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
} finally {
|
||||||
|
data = Files.readAllBytes(zipFile);
|
||||||
|
Files.delete(zipFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
return WebResponseUtils.bytesToWebResponse(
|
||||||
|
data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ByteArrayOutputStream currentDocToByteArray(PDDocument document) throws IOException {
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
document.save(baos);
|
||||||
|
document.close();
|
||||||
|
return baos;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
package stirling.software.SPDF.controller.api;
|
||||||
|
|
||||||
|
import java.awt.geom.AffineTransform;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import org.apache.pdfbox.multipdf.LayerUtility;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||||
|
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||||
|
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.model.api.PDFFile;
|
||||||
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/general")
|
||||||
|
@Tag(name = "General", description = "General APIs")
|
||||||
|
public class ToSinglePageController {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(ToSinglePageController.class);
|
||||||
|
|
||||||
|
@PostMapping(consumes = "multipart/form-data", value = "/pdf-to-single-page")
|
||||||
|
@Operation(
|
||||||
|
summary = "Convert a multi-page PDF into a single long page PDF",
|
||||||
|
description =
|
||||||
|
"This endpoint converts a multi-page PDF document into a single paged PDF document. The width of the single page will be same as the input's width, but the height will be the sum of all the pages' heights. Input:PDF Output:PDF Type:SISO")
|
||||||
|
public ResponseEntity<byte[]> pdfToSinglePage(@ModelAttribute PDFFile request)
|
||||||
|
throws IOException {
|
||||||
|
|
||||||
|
// Load the source document
|
||||||
|
PDDocument sourceDocument = PDDocument.load(request.getFileInput().getInputStream());
|
||||||
|
|
||||||
|
// Calculate total height and max width
|
||||||
|
float totalHeight = 0;
|
||||||
|
float maxWidth = 0;
|
||||||
|
for (PDPage page : sourceDocument.getPages()) {
|
||||||
|
PDRectangle pageSize = page.getMediaBox();
|
||||||
|
totalHeight += pageSize.getHeight();
|
||||||
|
maxWidth = Math.max(maxWidth, pageSize.getWidth());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new document and page with calculated dimensions
|
||||||
|
PDDocument newDocument = new PDDocument();
|
||||||
|
PDPage newPage = new PDPage(new PDRectangle(maxWidth, totalHeight));
|
||||||
|
newDocument.addPage(newPage);
|
||||||
|
|
||||||
|
// Initialize the content stream of the new page
|
||||||
|
PDPageContentStream contentStream = new PDPageContentStream(newDocument, newPage);
|
||||||
|
contentStream.close();
|
||||||
|
|
||||||
|
LayerUtility layerUtility = new LayerUtility(newDocument);
|
||||||
|
float yOffset = totalHeight;
|
||||||
|
|
||||||
|
// For each page, copy its content to the new page at the correct offset
|
||||||
|
for (PDPage page : sourceDocument.getPages()) {
|
||||||
|
PDFormXObject form =
|
||||||
|
layerUtility.importPageAsForm(
|
||||||
|
sourceDocument, sourceDocument.getPages().indexOf(page));
|
||||||
|
AffineTransform af =
|
||||||
|
AffineTransform.getTranslateInstance(
|
||||||
|
0, yOffset - page.getMediaBox().getHeight());
|
||||||
|
layerUtility.wrapInSaveRestore(newPage);
|
||||||
|
String defaultLayerName = "Layer" + sourceDocument.getPages().indexOf(page);
|
||||||
|
layerUtility.appendFormAsLayer(newPage, form, af, defaultLayerName);
|
||||||
|
yOffset -= page.getMediaBox().getHeight();
|
||||||
|
}
|
||||||
|
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
newDocument.save(baos);
|
||||||
|
newDocument.close();
|
||||||
|
sourceDocument.close();
|
||||||
|
|
||||||
|
byte[] result = baos.toByteArray();
|
||||||
|
return WebResponseUtils.bytesToWebResponse(
|
||||||
|
result,
|
||||||
|
request.getFileInput().getOriginalFilename().replaceFirst("[.][^.]+$", "")
|
||||||
|
+ "_singlePage.pdf");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,256 @@
|
|||||||
|
package stirling.software.SPDF.controller.api;
|
||||||
|
|
||||||
|
import java.security.Principal;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.ui.Model;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
|
||||||
|
import org.springframework.web.servlet.view.RedirectView;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import stirling.software.SPDF.config.security.UserService;
|
||||||
|
import stirling.software.SPDF.model.Role;
|
||||||
|
import stirling.software.SPDF.model.User;
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
@RequestMapping("/api/v1/user")
|
||||||
|
public class UserController {
|
||||||
|
|
||||||
|
@Autowired private UserService userService;
|
||||||
|
|
||||||
|
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
||||||
|
@PostMapping("/register")
|
||||||
|
public String register(
|
||||||
|
@RequestParam String username, @RequestParam String password, Model model) {
|
||||||
|
if (userService.usernameExists(username)) {
|
||||||
|
model.addAttribute("error", "Username already exists");
|
||||||
|
return "register";
|
||||||
|
}
|
||||||
|
|
||||||
|
userService.saveUser(username, password);
|
||||||
|
return "redirect:/login?registered=true";
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
||||||
|
@PostMapping("/change-username-and-password")
|
||||||
|
public RedirectView changeUsernameAndPassword(
|
||||||
|
Principal principal,
|
||||||
|
@RequestParam String currentPassword,
|
||||||
|
@RequestParam String newUsername,
|
||||||
|
@RequestParam String newPassword,
|
||||||
|
HttpServletRequest request,
|
||||||
|
HttpServletResponse response,
|
||||||
|
RedirectAttributes redirectAttributes) {
|
||||||
|
if (principal == null) {
|
||||||
|
return new RedirectView("/change-creds?messageType=notAuthenticated");
|
||||||
|
}
|
||||||
|
|
||||||
|
Optional<User> userOpt = userService.findByUsername(principal.getName());
|
||||||
|
|
||||||
|
if (userOpt == null || userOpt.isEmpty()) {
|
||||||
|
return new RedirectView("/change-creds?messageType=userNotFound");
|
||||||
|
}
|
||||||
|
|
||||||
|
User user = userOpt.get();
|
||||||
|
|
||||||
|
if (!userService.isPasswordCorrect(user, currentPassword)) {
|
||||||
|
return new RedirectView("/change-creds?messageType=incorrectPassword");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user.getUsername().equals(newUsername) && userService.usernameExists(newUsername)) {
|
||||||
|
return new RedirectView("/change-creds?messageType=usernameExists");
|
||||||
|
}
|
||||||
|
|
||||||
|
userService.changePassword(user, newPassword);
|
||||||
|
if (newUsername != null
|
||||||
|
&& newUsername.length() > 0
|
||||||
|
&& !user.getUsername().equals(newUsername)) {
|
||||||
|
userService.changeUsername(user, newUsername);
|
||||||
|
}
|
||||||
|
userService.changeFirstUse(user, false);
|
||||||
|
|
||||||
|
// Logout using Spring's utility
|
||||||
|
new SecurityContextLogoutHandler().logout(request, response, null);
|
||||||
|
|
||||||
|
return new RedirectView("/login?messageType=credsUpdated");
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
||||||
|
@PostMapping("/change-username")
|
||||||
|
public RedirectView changeUsername(
|
||||||
|
Principal principal,
|
||||||
|
@RequestParam String currentPassword,
|
||||||
|
@RequestParam String newUsername,
|
||||||
|
HttpServletRequest request,
|
||||||
|
HttpServletResponse response,
|
||||||
|
RedirectAttributes redirectAttributes) {
|
||||||
|
if (principal == null) {
|
||||||
|
return new RedirectView("/account?messageType=notAuthenticated");
|
||||||
|
}
|
||||||
|
|
||||||
|
Optional<User> userOpt = userService.findByUsername(principal.getName());
|
||||||
|
|
||||||
|
if (userOpt == null || userOpt.isEmpty()) {
|
||||||
|
return new RedirectView("/account?messageType=userNotFound");
|
||||||
|
}
|
||||||
|
|
||||||
|
User user = userOpt.get();
|
||||||
|
|
||||||
|
if (!userService.isPasswordCorrect(user, currentPassword)) {
|
||||||
|
return new RedirectView("/account?messageType=incorrectPassword");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user.getUsername().equals(newUsername) && userService.usernameExists(newUsername)) {
|
||||||
|
return new RedirectView("/account?messageType=usernameExists");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newUsername != null && newUsername.length() > 0) {
|
||||||
|
userService.changeUsername(user, newUsername);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logout using Spring's utility
|
||||||
|
new SecurityContextLogoutHandler().logout(request, response, null);
|
||||||
|
|
||||||
|
return new RedirectView("/login?messageType=credsUpdated");
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
||||||
|
@PostMapping("/change-password")
|
||||||
|
public RedirectView changePassword(
|
||||||
|
Principal principal,
|
||||||
|
@RequestParam String currentPassword,
|
||||||
|
@RequestParam String newPassword,
|
||||||
|
HttpServletRequest request,
|
||||||
|
HttpServletResponse response,
|
||||||
|
RedirectAttributes redirectAttributes) {
|
||||||
|
if (principal == null) {
|
||||||
|
return new RedirectView("/account?messageType=notAuthenticated");
|
||||||
|
}
|
||||||
|
|
||||||
|
Optional<User> userOpt = userService.findByUsername(principal.getName());
|
||||||
|
|
||||||
|
if (userOpt == null || userOpt.isEmpty()) {
|
||||||
|
return new RedirectView("/account?messageType=userNotFound");
|
||||||
|
}
|
||||||
|
|
||||||
|
User user = userOpt.get();
|
||||||
|
|
||||||
|
if (!userService.isPasswordCorrect(user, currentPassword)) {
|
||||||
|
return new RedirectView("/account?messageType=incorrectPassword");
|
||||||
|
}
|
||||||
|
|
||||||
|
userService.changePassword(user, newPassword);
|
||||||
|
|
||||||
|
// Logout using Spring's utility
|
||||||
|
new SecurityContextLogoutHandler().logout(request, response, null);
|
||||||
|
|
||||||
|
return new RedirectView("/login?messageType=credsUpdated");
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
||||||
|
@PostMapping("/updateUserSettings")
|
||||||
|
public String updateUserSettings(HttpServletRequest request, Principal principal) {
|
||||||
|
Map<String, String[]> paramMap = request.getParameterMap();
|
||||||
|
Map<String, String> updates = new HashMap<>();
|
||||||
|
|
||||||
|
System.out.println("Received parameter map: " + paramMap);
|
||||||
|
|
||||||
|
for (Map.Entry<String, String[]> entry : paramMap.entrySet()) {
|
||||||
|
updates.put(entry.getKey(), entry.getValue()[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
System.out.println("Processed updates: " + updates);
|
||||||
|
|
||||||
|
// Assuming you have a method in userService to update the settings for a user
|
||||||
|
userService.updateUserSettings(principal.getName(), updates);
|
||||||
|
|
||||||
|
return "redirect:/account"; // Redirect to a page of your choice after updating
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||||
|
@PostMapping("/admin/saveUser")
|
||||||
|
public RedirectView saveUser(
|
||||||
|
@RequestParam String username,
|
||||||
|
@RequestParam String password,
|
||||||
|
@RequestParam String role,
|
||||||
|
@RequestParam(name = "forceChange", required = false, defaultValue = "false")
|
||||||
|
boolean forceChange) {
|
||||||
|
|
||||||
|
if (userService.usernameExists(username)) {
|
||||||
|
return new RedirectView("/addUsers?messageType=usernameExists");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// Validate the role
|
||||||
|
Role roleEnum = Role.fromString(role);
|
||||||
|
if (roleEnum == Role.INTERNAL_API_USER) {
|
||||||
|
// If the role is INTERNAL_API_USER, reject the request
|
||||||
|
return new RedirectView("/addUsers?messageType=invalidRole");
|
||||||
|
}
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
// If the role ID is not valid, redirect with an error message
|
||||||
|
return new RedirectView("/addUsers?messageType=invalidRole");
|
||||||
|
}
|
||||||
|
|
||||||
|
userService.saveUser(username, password, role, forceChange);
|
||||||
|
return new RedirectView("/addUsers"); // Redirect to account page after adding the user
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||||
|
@PostMapping("/admin/deleteUser/{username}")
|
||||||
|
public String deleteUser(@PathVariable String username, Authentication authentication) {
|
||||||
|
|
||||||
|
// Get the currently authenticated username
|
||||||
|
String currentUsername = authentication.getName();
|
||||||
|
|
||||||
|
// Check if the provided username matches the current session's username
|
||||||
|
if (currentUsername.equals(username)) {
|
||||||
|
throw new IllegalArgumentException("Cannot delete currently logined in user.");
|
||||||
|
}
|
||||||
|
|
||||||
|
userService.deleteUser(username);
|
||||||
|
return "redirect:/addUsers";
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
||||||
|
@PostMapping("/get-api-key")
|
||||||
|
public ResponseEntity<String> getApiKey(Principal principal) {
|
||||||
|
if (principal == null) {
|
||||||
|
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("User not authenticated.");
|
||||||
|
}
|
||||||
|
String username = principal.getName();
|
||||||
|
String apiKey = userService.getApiKeyForUser(username);
|
||||||
|
if (apiKey == null) {
|
||||||
|
return ResponseEntity.status(HttpStatus.NOT_FOUND).body("API key not found for user.");
|
||||||
|
}
|
||||||
|
return ResponseEntity.ok(apiKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
||||||
|
@PostMapping("/update-api-key")
|
||||||
|
public ResponseEntity<String> updateApiKey(Principal principal) {
|
||||||
|
if (principal == null) {
|
||||||
|
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("User not authenticated.");
|
||||||
|
}
|
||||||
|
String username = principal.getName();
|
||||||
|
User user = userService.refreshApiKeyForUser(username);
|
||||||
|
String apiKey = user.getApiKey();
|
||||||
|
if (apiKey == null) {
|
||||||
|
return ResponseEntity.status(HttpStatus.NOT_FOUND).body("API key not found for user.");
|
||||||
|
}
|
||||||
|
return ResponseEntity.ok(apiKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package stirling.software.SPDF.controller.api.converters;
|
||||||
|
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.model.api.GeneralFile;
|
||||||
|
import stirling.software.SPDF.utils.FileToPdf;
|
||||||
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@Tag(name = "Convert", description = "Convert APIs")
|
||||||
|
@RequestMapping("/api/v1/convert")
|
||||||
|
public class ConvertHtmlToPDF {
|
||||||
|
|
||||||
|
@PostMapping(consumes = "multipart/form-data", value = "/html/pdf")
|
||||||
|
@Operation(
|
||||||
|
summary = "Convert an HTML or ZIP (containing HTML and CSS) to PDF",
|
||||||
|
description =
|
||||||
|
"This endpoint takes an HTML or ZIP file input and converts it to a PDF format.")
|
||||||
|
public ResponseEntity<byte[]> HtmlToPdf(@ModelAttribute GeneralFile request) throws Exception {
|
||||||
|
MultipartFile fileInput = request.getFileInput();
|
||||||
|
|
||||||
|
if (fileInput == null) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Please provide an HTML or ZIP file for conversion.");
|
||||||
|
}
|
||||||
|
|
||||||
|
String originalFilename = fileInput.getOriginalFilename();
|
||||||
|
if (originalFilename == null
|
||||||
|
|| (!originalFilename.endsWith(".html") && !originalFilename.endsWith(".zip"))) {
|
||||||
|
throw new IllegalArgumentException("File must be either .html or .zip format.");
|
||||||
|
}
|
||||||
|
byte[] pdfBytes = FileToPdf.convertHtmlToPdf(fileInput.getBytes(), originalFilename);
|
||||||
|
|
||||||
|
String outputFilename =
|
||||||
|
originalFilename.replaceFirst("[.][^.]+$", "")
|
||||||
|
+ ".pdf"; // Remove file extension and append .pdf
|
||||||
|
|
||||||
|
return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package stirling.software.SPDF.controller.api.converters;
|
package stirling.software.SPDF.controller.api.converters;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.net.URLConnection;
|
||||||
|
|
||||||
import org.apache.pdfbox.rendering.ImageType;
|
import org.apache.pdfbox.rendering.ImageType;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@@ -11,40 +12,39 @@ import org.springframework.http.HttpHeaders;
|
|||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
|
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.RequestParam;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
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.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.media.Schema;
|
|
||||||
|
import stirling.software.SPDF.model.api.converters.ConvertToImageRequest;
|
||||||
|
import stirling.software.SPDF.model.api.converters.ConvertToPdfRequest;
|
||||||
import stirling.software.SPDF.utils.PdfUtils;
|
import stirling.software.SPDF.utils.PdfUtils;
|
||||||
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/convert")
|
||||||
|
@Tag(name = "Convert", description = "Convert APIs")
|
||||||
public class ConvertImgPDFController {
|
public class ConvertImgPDFController {
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(ConvertImgPDFController.class);
|
private static final Logger logger = LoggerFactory.getLogger(ConvertImgPDFController.class);
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/pdf-to-img")
|
@PostMapping(consumes = "multipart/form-data", value = "/pdf/img")
|
||||||
@Operation(summary = "Convert PDF to image(s)",
|
@Operation(
|
||||||
description = "This endpoint converts a PDF file to image(s) with the specified image format, color type, and DPI. Users can choose to get a single image or multiple images.")
|
summary = "Convert PDF to image(s)",
|
||||||
public ResponseEntity<Resource> convertToImage(
|
description =
|
||||||
@RequestPart(required = true, value = "fileInput")
|
"This endpoint converts a PDF file to image(s) with the specified image format, color type, and DPI. Users can choose to get a single image or multiple images. Input:PDF Output:Image Type:SI-Conditional")
|
||||||
@Parameter(description = "The input PDF file to be converted")
|
public ResponseEntity<Resource> convertToImage(@ModelAttribute ConvertToImageRequest request)
|
||||||
MultipartFile file,
|
throws IOException {
|
||||||
@RequestParam("imageFormat")
|
MultipartFile file = request.getFileInput();
|
||||||
@Parameter(description = "The output image format", schema = @Schema(allowableValues = {"png", "jpeg", "jpg", "gif"}))
|
String imageFormat = request.getImageFormat();
|
||||||
String imageFormat,
|
String singleOrMultiple = request.getSingleOrMultiple();
|
||||||
@RequestParam("singleOrMultiple")
|
String colorType = request.getColorType();
|
||||||
@Parameter(description = "Choose between a single image containing all pages or separate images for each page", schema = @Schema(allowableValues = {"single", "multiple"}))
|
String dpi = request.getDpi();
|
||||||
String singleOrMultiple,
|
|
||||||
@RequestParam("colorType")
|
|
||||||
@Parameter(description = "The color type of the output image(s)", schema = @Schema(allowableValues = {"rgb", "greyscale", "blackwhite"}))
|
|
||||||
String colorType,
|
|
||||||
@RequestParam("dpi")
|
|
||||||
@Parameter(description = "The DPI (dots per inch) for the output image(s)")
|
|
||||||
String dpi) throws IOException {
|
|
||||||
|
|
||||||
byte[] pdfBytes = file.getBytes();
|
byte[] pdfBytes = file.getBytes();
|
||||||
ImageType colorTypeResult = ImageType.RGB;
|
ImageType colorTypeResult = ImageType.RGB;
|
||||||
@@ -56,8 +56,16 @@ public class ConvertImgPDFController {
|
|||||||
// returns bytes for image
|
// returns bytes for image
|
||||||
boolean singleImage = singleOrMultiple.equals("single");
|
boolean singleImage = singleOrMultiple.equals("single");
|
||||||
byte[] result = null;
|
byte[] result = null;
|
||||||
|
String filename = file.getOriginalFilename().replaceFirst("[.][^.]+$", "");
|
||||||
try {
|
try {
|
||||||
result = PdfUtils.convertFromPdf(pdfBytes, imageFormat.toUpperCase(), colorTypeResult, singleImage, Integer.valueOf(dpi));
|
result =
|
||||||
|
PdfUtils.convertFromPdf(
|
||||||
|
pdfBytes,
|
||||||
|
imageFormat.toUpperCase(),
|
||||||
|
colorTypeResult,
|
||||||
|
singleImage,
|
||||||
|
Integer.valueOf(dpi),
|
||||||
|
filename);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
// TODO Auto-generated catch block
|
// TODO Auto-generated catch block
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
@@ -68,48 +76,43 @@ public class ConvertImgPDFController {
|
|||||||
if (singleImage) {
|
if (singleImage) {
|
||||||
HttpHeaders headers = new HttpHeaders();
|
HttpHeaders headers = new HttpHeaders();
|
||||||
headers.setContentType(MediaType.parseMediaType(getMediaType(imageFormat)));
|
headers.setContentType(MediaType.parseMediaType(getMediaType(imageFormat)));
|
||||||
ResponseEntity<Resource> response = new ResponseEntity<>(new ByteArrayResource(result), headers, HttpStatus.OK);
|
ResponseEntity<Resource> response =
|
||||||
|
new ResponseEntity<>(new ByteArrayResource(result), headers, HttpStatus.OK);
|
||||||
return response;
|
return response;
|
||||||
} else {
|
} else {
|
||||||
ByteArrayResource resource = new ByteArrayResource(result);
|
ByteArrayResource resource = new ByteArrayResource(result);
|
||||||
// return the Resource in the response
|
// return the Resource in the response
|
||||||
return ResponseEntity.ok()
|
return ResponseEntity.ok()
|
||||||
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_convertedToImages.zip")
|
.header(
|
||||||
.contentType(MediaType.APPLICATION_OCTET_STREAM).contentLength(resource.contentLength()).body(resource);
|
HttpHeaders.CONTENT_DISPOSITION,
|
||||||
|
"attachment; filename=" + filename + "_convertedToImages.zip")
|
||||||
|
.contentType(MediaType.APPLICATION_OCTET_STREAM)
|
||||||
|
.contentLength(resource.contentLength())
|
||||||
|
.body(resource);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/img-to-pdf")
|
@PostMapping(consumes = "multipart/form-data", value = "/img/pdf")
|
||||||
@Operation(summary = "Convert images to a PDF file",
|
@Operation(
|
||||||
description = "This endpoint converts one or more images to a PDF file. Users can specify whether to stretch the images to fit the PDF page, and whether to automatically rotate the images.")
|
summary = "Convert images to a PDF file",
|
||||||
public ResponseEntity<byte[]> convertToPdf(
|
description =
|
||||||
@RequestPart(required = true, value = "fileInput")
|
"This endpoint converts one or more images to a PDF file. Users can specify whether to stretch the images to fit the PDF page, and whether to automatically rotate the images. Input:Image Output:PDF Type:SISO?")
|
||||||
@Parameter(description = "The input images to be converted to a PDF file")
|
public ResponseEntity<byte[]> convertToPdf(@ModelAttribute ConvertToPdfRequest request)
|
||||||
MultipartFile[] file,
|
throws IOException {
|
||||||
@RequestParam(defaultValue = "false", name = "stretchToFit")
|
MultipartFile[] file = request.getFileInput();
|
||||||
@Parameter(description = "Whether to stretch the images to fit the PDF page or maintain the aspect ratio", example = "false")
|
String fitOption = request.getFitOption();
|
||||||
boolean stretchToFit,
|
String colorType = request.getColorType();
|
||||||
@RequestParam("colorType")
|
boolean autoRotate = request.isAutoRotate();
|
||||||
@Parameter(description = "The color type of the output image(s)", schema = @Schema(allowableValues = {"rgb", "greyscale", "blackwhite"}))
|
|
||||||
String colorType,
|
|
||||||
@RequestParam(defaultValue = "false", name = "autoRotate")
|
|
||||||
@Parameter(description = "Whether to automatically rotate the images to better fit the PDF page", example = "true")
|
|
||||||
boolean autoRotate) throws IOException {
|
|
||||||
// Convert the file to PDF and get the resulting bytes
|
// Convert the file to PDF and get the resulting bytes
|
||||||
byte[] bytes = PdfUtils.imageToPdf(file, stretchToFit, autoRotate, colorType);
|
byte[] bytes = PdfUtils.imageToPdf(file, fitOption, autoRotate, colorType);
|
||||||
return PdfUtils.bytesToWebResponse(bytes, file[0].getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_coverted.pdf");
|
return WebResponseUtils.bytesToWebResponse(
|
||||||
|
bytes,
|
||||||
|
file[0].getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_converted.pdf");
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getMediaType(String imageFormat) {
|
private String getMediaType(String imageFormat) {
|
||||||
if (imageFormat.equalsIgnoreCase("PNG"))
|
String mimeType = URLConnection.guessContentTypeFromName("." + imageFormat);
|
||||||
return "image/png";
|
return mimeType.equals("null") ? "application/octet-stream" : mimeType;
|
||||||
else if (imageFormat.equalsIgnoreCase("JPEG") || imageFormat.equalsIgnoreCase("JPG"))
|
|
||||||
return "image/jpeg";
|
|
||||||
else if (imageFormat.equalsIgnoreCase("GIF"))
|
|
||||||
return "image/gif";
|
|
||||||
else
|
|
||||||
return "application/octet-stream";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package stirling.software.SPDF.controller.api.converters;
|
||||||
|
|
||||||
|
import org.commonmark.node.Node;
|
||||||
|
import org.commonmark.parser.Parser;
|
||||||
|
import org.commonmark.renderer.html.HtmlRenderer;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.model.api.GeneralFile;
|
||||||
|
import stirling.software.SPDF.utils.FileToPdf;
|
||||||
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@Tag(name = "Convert", description = "Convert APIs")
|
||||||
|
@RequestMapping("/api/v1/convert")
|
||||||
|
public class ConvertMarkdownToPdf {
|
||||||
|
|
||||||
|
@PostMapping(consumes = "multipart/form-data", value = "/markdown/pdf")
|
||||||
|
@Operation(
|
||||||
|
summary = "Convert a Markdown file to PDF",
|
||||||
|
description =
|
||||||
|
"This endpoint takes a Markdown file input, converts it to HTML, and then to PDF format.")
|
||||||
|
public ResponseEntity<byte[]> markdownToPdf(@ModelAttribute GeneralFile request)
|
||||||
|
throws Exception {
|
||||||
|
MultipartFile fileInput = request.getFileInput();
|
||||||
|
|
||||||
|
if (fileInput == null) {
|
||||||
|
throw new IllegalArgumentException("Please provide a Markdown file for conversion.");
|
||||||
|
}
|
||||||
|
|
||||||
|
String originalFilename = fileInput.getOriginalFilename();
|
||||||
|
if (originalFilename == null || !originalFilename.endsWith(".md")) {
|
||||||
|
throw new IllegalArgumentException("File must be in .md format.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert Markdown to HTML using CommonMark
|
||||||
|
Parser parser = Parser.builder().build();
|
||||||
|
Node document = parser.parse(new String(fileInput.getBytes()));
|
||||||
|
HtmlRenderer renderer = HtmlRenderer.builder().build();
|
||||||
|
String htmlContent = renderer.render(document);
|
||||||
|
|
||||||
|
byte[] pdfBytes = FileToPdf.convertHtmlToPdf(htmlContent.getBytes(), "converted.html");
|
||||||
|
|
||||||
|
String outputFilename =
|
||||||
|
originalFilename.replaceFirst("[.][^.]+$", "")
|
||||||
|
+ ".pdf"; // Remove file extension and append .pdf
|
||||||
|
return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,36 +10,55 @@ import java.util.List;
|
|||||||
|
|
||||||
import org.apache.commons.io.FilenameUtils;
|
import org.apache.commons.io.FilenameUtils;
|
||||||
import org.springframework.http.ResponseEntity;
|
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.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestPart;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
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.Parameter;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import stirling.software.SPDF.utils.PdfUtils;
|
|
||||||
|
import stirling.software.SPDF.model.api.GeneralFile;
|
||||||
import stirling.software.SPDF.utils.ProcessExecutor;
|
import stirling.software.SPDF.utils.ProcessExecutor;
|
||||||
|
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
|
||||||
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
|
@Tag(name = "Convert", description = "Convert APIs")
|
||||||
|
@RequestMapping("/api/v1/convert")
|
||||||
public class ConvertOfficeController {
|
public class ConvertOfficeController {
|
||||||
|
|
||||||
public byte[] convertToPdf(MultipartFile inputFile) throws IOException, InterruptedException {
|
public byte[] convertToPdf(MultipartFile inputFile) throws IOException, InterruptedException {
|
||||||
// Check for valid file extension
|
// Check for valid file extension
|
||||||
String originalFilename = inputFile.getOriginalFilename();
|
String originalFilename = inputFile.getOriginalFilename();
|
||||||
if (originalFilename == null || !isValidFileExtension(FilenameUtils.getExtension(originalFilename))) {
|
if (originalFilename == null
|
||||||
|
|| !isValidFileExtension(FilenameUtils.getExtension(originalFilename))) {
|
||||||
throw new IllegalArgumentException("Invalid file extension");
|
throw new IllegalArgumentException("Invalid file extension");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save the uploaded file to a temporary location
|
// Save the uploaded file to a temporary location
|
||||||
Path tempInputFile = Files.createTempFile("input_", "." + FilenameUtils.getExtension(originalFilename));
|
Path tempInputFile =
|
||||||
|
Files.createTempFile("input_", "." + FilenameUtils.getExtension(originalFilename));
|
||||||
Files.copy(inputFile.getInputStream(), tempInputFile, StandardCopyOption.REPLACE_EXISTING);
|
Files.copy(inputFile.getInputStream(), tempInputFile, StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
|
||||||
// Prepare the output file path
|
// Prepare the output file path
|
||||||
Path tempOutputFile = Files.createTempFile("output_", ".pdf");
|
Path tempOutputFile = Files.createTempFile("output_", ".pdf");
|
||||||
|
|
||||||
// Run the LibreOffice command
|
// Run the LibreOffice command
|
||||||
List<String> command = new ArrayList<>(Arrays.asList("unoconv", "-vvv", "-f", "pdf", "-o", tempOutputFile.toString(), tempInputFile.toString()));
|
List<String> command =
|
||||||
int returnCode = ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE).runCommandWithOutputHandling(command);
|
new ArrayList<>(
|
||||||
|
Arrays.asList(
|
||||||
|
"unoconv",
|
||||||
|
"-vvv",
|
||||||
|
"-f",
|
||||||
|
"pdf",
|
||||||
|
"-o",
|
||||||
|
tempOutputFile.toString(),
|
||||||
|
tempInputFile.toString()));
|
||||||
|
ProcessExecutorResult returnCode =
|
||||||
|
ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE)
|
||||||
|
.runCommandWithOutputHandling(command);
|
||||||
|
|
||||||
// Read the converted PDF file
|
// Read the converted PDF file
|
||||||
byte[] pdfBytes = Files.readAllBytes(tempOutputFile);
|
byte[] pdfBytes = Files.readAllBytes(tempOutputFile);
|
||||||
@@ -50,29 +69,27 @@ public class ConvertOfficeController {
|
|||||||
|
|
||||||
return pdfBytes;
|
return pdfBytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isValidFileExtension(String fileExtension) {
|
private boolean isValidFileExtension(String fileExtension) {
|
||||||
String extensionPattern = "^(?i)[a-z0-9]{2,4}$";
|
String extensionPattern = "^(?i)[a-z0-9]{2,4}$";
|
||||||
return fileExtension.matches(extensionPattern);
|
return fileExtension.matches(extensionPattern);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/file-to-pdf")
|
@PostMapping(consumes = "multipart/form-data", value = "/file/pdf")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Convert a file to a PDF using OCR",
|
summary = "Convert a file to a PDF using LibreOffice",
|
||||||
description = "This endpoint converts a given file to a PDF using Optical Character Recognition (OCR). The filename of the resulting PDF will be the original filename with '_convertedToPDF.pdf' appended."
|
description =
|
||||||
)
|
"This endpoint converts a given file to a PDF using LibreOffice API Input:Any Output:PDF Type:SISO")
|
||||||
public ResponseEntity<byte[]> processPdfWithOCR(
|
public ResponseEntity<byte[]> processFileToPDF(@ModelAttribute GeneralFile request)
|
||||||
@RequestPart(required = true, value = "fileInput")
|
throws Exception {
|
||||||
@Parameter(
|
MultipartFile inputFile = request.getFileInput();
|
||||||
description = "The input file to be converted to a PDF file using OCR",
|
|
||||||
required = true
|
|
||||||
)
|
|
||||||
MultipartFile inputFile
|
|
||||||
) throws IOException, InterruptedException {
|
|
||||||
// unused but can start server instance if startup time is to long
|
// unused but can start server instance if startup time is to long
|
||||||
// LibreOfficeListener.getInstance().start();
|
// LibreOfficeListener.getInstance().start();
|
||||||
|
|
||||||
byte[] pdfByteArray = convertToPdf(inputFile);
|
byte[] pdfByteArray = convertToPdf(inputFile);
|
||||||
return PdfUtils.bytesToWebResponse(pdfByteArray, inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_convertedToPDF.pdf");
|
return WebResponseUtils.bytesToWebResponse(
|
||||||
|
pdfByteArray,
|
||||||
|
inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "")
|
||||||
|
+ "_convertedToPDF.pdf");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,70 +3,90 @@ package stirling.software.SPDF.controller.api.converters;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
import org.springframework.http.ResponseEntity;
|
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.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
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.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.media.Schema;
|
|
||||||
|
import stirling.software.SPDF.model.api.PDFFile;
|
||||||
|
import stirling.software.SPDF.model.api.converters.PdfToPresentationRequest;
|
||||||
|
import stirling.software.SPDF.model.api.converters.PdfToTextOrRTFRequest;
|
||||||
|
import stirling.software.SPDF.model.api.converters.PdfToWordRequest;
|
||||||
import stirling.software.SPDF.utils.PDFToFile;
|
import stirling.software.SPDF.utils.PDFToFile;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/convert")
|
||||||
|
@Tag(name = "Convert", description = "Convert APIs")
|
||||||
public class ConvertPDFToOffice {
|
public class ConvertPDFToOffice {
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/pdf-to-html")
|
@PostMapping(consumes = "multipart/form-data", value = "/pdf/html")
|
||||||
@Operation(summary = "Convert PDF to HTML", description = "This endpoint converts a PDF file to HTML format.")
|
@Operation(
|
||||||
public ResponseEntity<byte[]> processPdfToHTML(
|
summary = "Convert PDF to HTML",
|
||||||
@RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file to be converted to HTML format", required = true) MultipartFile inputFile)
|
description =
|
||||||
throws IOException, InterruptedException {
|
"This endpoint converts a PDF file to HTML format. Input:PDF Output:HTML Type:SISO")
|
||||||
PDFToFile pdfToFile = new PDFToFile();
|
public ResponseEntity<byte[]> processPdfToHTML(@ModelAttribute PDFFile request)
|
||||||
return pdfToFile.processPdfToOfficeFormat(inputFile, "html", "writer_pdf_import");
|
throws Exception {
|
||||||
}
|
MultipartFile inputFile = request.getFileInput();
|
||||||
|
PDFToFile pdfToFile = new PDFToFile();
|
||||||
|
return pdfToFile.processPdfToOfficeFormat(inputFile, "html", "writer_pdf_import");
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/pdf-to-presentation")
|
@PostMapping(consumes = "multipart/form-data", value = "/pdf/presentation")
|
||||||
@Operation(summary = "Convert PDF to Presentation format", description = "This endpoint converts a given PDF file to a Presentation format.")
|
@Operation(
|
||||||
public ResponseEntity<byte[]> processPdfToPresentation(
|
summary = "Convert PDF to Presentation format",
|
||||||
@RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file") MultipartFile inputFile,
|
description =
|
||||||
@RequestParam("outputFormat") @Parameter(description = "The output Presentation format", schema = @Schema(allowableValues = {
|
"This endpoint converts a given PDF file to a Presentation format. Input:PDF Output:PPT Type:SISO")
|
||||||
"ppt", "pptx", "odp" })) String outputFormat)
|
public ResponseEntity<byte[]> processPdfToPresentation(
|
||||||
throws IOException, InterruptedException {
|
@ModelAttribute PdfToPresentationRequest request)
|
||||||
PDFToFile pdfToFile = new PDFToFile();
|
throws IOException, InterruptedException {
|
||||||
return pdfToFile.processPdfToOfficeFormat(inputFile, outputFormat, "impress_pdf_import");
|
MultipartFile inputFile = request.getFileInput();
|
||||||
}
|
String outputFormat = request.getOutputFormat();
|
||||||
|
PDFToFile pdfToFile = new PDFToFile();
|
||||||
|
return pdfToFile.processPdfToOfficeFormat(inputFile, outputFormat, "impress_pdf_import");
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/pdf-to-text")
|
@PostMapping(consumes = "multipart/form-data", value = "/pdf/text")
|
||||||
@Operation(summary = "Convert PDF to Text or RTF format", description = "This endpoint converts a given PDF file to Text or RTF format.")
|
@Operation(
|
||||||
public ResponseEntity<byte[]> processPdfToRTForTXT(
|
summary = "Convert PDF to Text or RTF format",
|
||||||
@RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file") MultipartFile inputFile,
|
description =
|
||||||
@RequestParam("outputFormat") @Parameter(description = "The output Text or RTF format", schema = @Schema(allowableValues = {
|
"This endpoint converts a given PDF file to Text or RTF format. Input:PDF Output:TXT Type:SISO")
|
||||||
"rtf", "txt:Text" })) String outputFormat)
|
public ResponseEntity<byte[]> processPdfToRTForTXT(
|
||||||
throws IOException, InterruptedException {
|
@ModelAttribute PdfToTextOrRTFRequest request)
|
||||||
PDFToFile pdfToFile = new PDFToFile();
|
throws IOException, InterruptedException {
|
||||||
return pdfToFile.processPdfToOfficeFormat(inputFile, outputFormat, "writer_pdf_import");
|
MultipartFile inputFile = request.getFileInput();
|
||||||
}
|
String outputFormat = request.getOutputFormat();
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/pdf-to-word")
|
PDFToFile pdfToFile = new PDFToFile();
|
||||||
@Operation(summary = "Convert PDF to Word document", description = "This endpoint converts a given PDF file to a Word document format.")
|
return pdfToFile.processPdfToOfficeFormat(inputFile, outputFormat, "writer_pdf_import");
|
||||||
public ResponseEntity<byte[]> processPdfToWord(
|
}
|
||||||
@RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file") MultipartFile inputFile,
|
|
||||||
@RequestParam("outputFormat") @Parameter(description = "The output Word document format", schema = @Schema(allowableValues = {
|
|
||||||
"doc", "docx", "odt" })) String outputFormat)
|
|
||||||
throws IOException, InterruptedException {
|
|
||||||
PDFToFile pdfToFile = new PDFToFile();
|
|
||||||
return pdfToFile.processPdfToOfficeFormat(inputFile, outputFormat, "writer_pdf_import");
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/pdf-to-xml")
|
@PostMapping(consumes = "multipart/form-data", value = "/pdf/word")
|
||||||
@Operation(summary = "Convert PDF to XML", description = "This endpoint converts a PDF file to an XML file.")
|
@Operation(
|
||||||
public ResponseEntity<byte[]> processPdfToXML(
|
summary = "Convert PDF to Word document",
|
||||||
@RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file to be converted to an XML file", required = true) MultipartFile inputFile)
|
description =
|
||||||
throws IOException, InterruptedException {
|
"This endpoint converts a given PDF file to a Word document format. Input:PDF Output:WORD Type:SISO")
|
||||||
|
public ResponseEntity<byte[]> processPdfToWord(@ModelAttribute PdfToWordRequest request)
|
||||||
|
throws IOException, InterruptedException {
|
||||||
|
MultipartFile inputFile = request.getFileInput();
|
||||||
|
String outputFormat = request.getOutputFormat();
|
||||||
|
PDFToFile pdfToFile = new PDFToFile();
|
||||||
|
return pdfToFile.processPdfToOfficeFormat(inputFile, outputFormat, "writer_pdf_import");
|
||||||
|
}
|
||||||
|
|
||||||
PDFToFile pdfToFile = new PDFToFile();
|
@PostMapping(consumes = "multipart/form-data", value = "/pdf/xml")
|
||||||
return pdfToFile.processPdfToOfficeFormat(inputFile, "xml", "writer_pdf_import");
|
@Operation(
|
||||||
}
|
summary = "Convert PDF to XML",
|
||||||
|
description =
|
||||||
|
"This endpoint converts a PDF file to an XML file. Input:PDF Output:XML Type:SISO")
|
||||||
|
public ResponseEntity<byte[]> processPdfToXML(@ModelAttribute PDFFile request)
|
||||||
|
throws Exception {
|
||||||
|
MultipartFile inputFile = request.getFileInput();
|
||||||
|
|
||||||
|
PDFToFile pdfToFile = new PDFToFile();
|
||||||
|
return pdfToFile.processPdfToOfficeFormat(inputFile, "xml", "writer_pdf_import");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +1,37 @@
|
|||||||
package stirling.software.SPDF.controller.api.converters;
|
package stirling.software.SPDF.controller.api.converters;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
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;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import org.springframework.http.ResponseEntity;
|
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.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestPart;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
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.Parameter;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import stirling.software.SPDF.utils.PdfUtils;
|
|
||||||
|
import stirling.software.SPDF.model.api.PDFFile;
|
||||||
import stirling.software.SPDF.utils.ProcessExecutor;
|
import stirling.software.SPDF.utils.ProcessExecutor;
|
||||||
|
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
|
||||||
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/convert")
|
||||||
|
@Tag(name = "Convert", description = "Convert APIs")
|
||||||
public class ConvertPDFToPDFA {
|
public class ConvertPDFToPDFA {
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/pdf-to-pdfa")
|
@PostMapping(consumes = "multipart/form-data", value = "/pdf/pdfa")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Convert a PDF to a PDF/A",
|
summary = "Convert a PDF to a PDF/A",
|
||||||
description = "This endpoint converts a PDF file to a PDF/A file. PDF/A is a format designed for long-term archiving of digital documents."
|
description =
|
||||||
)
|
"This endpoint converts a PDF file to a PDF/A file. PDF/A is a format designed for long-term archiving of digital documents. Input:PDF Output:PDF Type:SISO")
|
||||||
public ResponseEntity<byte[]> pdfToPdfA(
|
public ResponseEntity<byte[]> pdfToPdfA(@ModelAttribute PDFFile request) throws Exception {
|
||||||
@RequestPart(required = true, value = "fileInput")
|
MultipartFile inputFile = request.getFileInput();
|
||||||
@Parameter(description = "The input PDF file to be converted to a PDF/A file", required = true)
|
|
||||||
MultipartFile inputFile) throws IOException, InterruptedException {
|
|
||||||
|
|
||||||
// Save the uploaded file to a temporary location
|
// Save the uploaded file to a temporary location
|
||||||
Path tempInputFile = Files.createTempFile("input_", ".pdf");
|
Path tempInputFile = Files.createTempFile("input_", ".pdf");
|
||||||
@@ -47,7 +50,9 @@ public class ConvertPDFToPDFA {
|
|||||||
command.add(tempInputFile.toString());
|
command.add(tempInputFile.toString());
|
||||||
command.add(tempOutputFile.toString());
|
command.add(tempOutputFile.toString());
|
||||||
|
|
||||||
int returnCode = ProcessExecutor.getInstance(ProcessExecutor.Processes.OCR_MY_PDF).runCommandWithOutputHandling(command);
|
ProcessExecutorResult returnCode =
|
||||||
|
ProcessExecutor.getInstance(ProcessExecutor.Processes.OCR_MY_PDF)
|
||||||
|
.runCommandWithOutputHandling(command);
|
||||||
|
|
||||||
// Read the optimized PDF file
|
// Read the optimized PDF file
|
||||||
byte[] pdfBytes = Files.readAllBytes(tempOutputFile);
|
byte[] pdfBytes = Files.readAllBytes(tempOutputFile);
|
||||||
@@ -57,8 +62,8 @@ public class ConvertPDFToPDFA {
|
|||||||
Files.delete(tempOutputFile);
|
Files.delete(tempOutputFile);
|
||||||
|
|
||||||
// Return the optimized PDF as a response
|
// Return the optimized PDF as a response
|
||||||
String outputFilename = inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_PDFA.pdf";
|
String outputFilename =
|
||||||
return PdfUtils.bytesToWebResponse(pdfBytes, outputFilename);
|
inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_PDFA.pdf";
|
||||||
|
return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package stirling.software.SPDF.controller.api.converters;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.model.api.converters.UrlToPdfRequest;
|
||||||
|
import stirling.software.SPDF.utils.GeneralUtils;
|
||||||
|
import stirling.software.SPDF.utils.ProcessExecutor;
|
||||||
|
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
|
||||||
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@Tag(name = "Convert", description = "Convert APIs")
|
||||||
|
@RequestMapping("/api/v1/convert")
|
||||||
|
public class ConvertWebsiteToPDF {
|
||||||
|
|
||||||
|
@PostMapping(consumes = "multipart/form-data", value = "/url/pdf")
|
||||||
|
@Operation(
|
||||||
|
summary = "Convert a URL to a PDF",
|
||||||
|
description =
|
||||||
|
"This endpoint fetches content from a URL and converts it to a PDF format. Input:N/A Output:PDF Type:SISO")
|
||||||
|
public ResponseEntity<byte[]> urlToPdf(@ModelAttribute UrlToPdfRequest request)
|
||||||
|
throws IOException, InterruptedException {
|
||||||
|
String URL = request.getUrlInput();
|
||||||
|
|
||||||
|
// Validate the URL format
|
||||||
|
if (!URL.matches("^https?://.*") || !GeneralUtils.isValidURL(URL)) {
|
||||||
|
throw new IllegalArgumentException("Invalid URL format provided.");
|
||||||
|
}
|
||||||
|
Path tempOutputFile = null;
|
||||||
|
byte[] pdfBytes;
|
||||||
|
try {
|
||||||
|
// Prepare the output file path
|
||||||
|
tempOutputFile = Files.createTempFile("output_", ".pdf");
|
||||||
|
|
||||||
|
// Prepare the OCRmyPDF command
|
||||||
|
List<String> command = new ArrayList<>();
|
||||||
|
command.add("weasyprint");
|
||||||
|
command.add(URL);
|
||||||
|
command.add(tempOutputFile.toString());
|
||||||
|
|
||||||
|
ProcessExecutorResult returnCode =
|
||||||
|
ProcessExecutor.getInstance(ProcessExecutor.Processes.WEASYPRINT)
|
||||||
|
.runCommandWithOutputHandling(command);
|
||||||
|
|
||||||
|
// Read the optimized PDF file
|
||||||
|
pdfBytes = Files.readAllBytes(tempOutputFile);
|
||||||
|
} finally {
|
||||||
|
// Clean up the temporary files
|
||||||
|
Files.delete(tempOutputFile);
|
||||||
|
}
|
||||||
|
// Convert URL to a safe filename
|
||||||
|
String outputFilename = convertURLToFileName(URL);
|
||||||
|
|
||||||
|
return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String convertURLToFileName(String url) {
|
||||||
|
String safeName = url.replaceAll("[^a-zA-Z0-9]", "_");
|
||||||
|
if (safeName.length() > 50) {
|
||||||
|
safeName = safeName.substring(0, 50); // restrict to 50 characters
|
||||||
|
}
|
||||||
|
return safeName + ".pdf";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
package stirling.software.SPDF.controller.api.converters;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.StringWriter;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.http.ContentDisposition;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import com.opencsv.CSVWriter;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.controller.api.CropController;
|
||||||
|
import stirling.software.SPDF.controller.api.strippers.PDFTableStripper;
|
||||||
|
import stirling.software.SPDF.model.api.extract.PDFFilePage;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/convert")
|
||||||
|
@Tag(name = "Convert", description = "Convert APIs")
|
||||||
|
public class ExtractController {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(CropController.class);
|
||||||
|
|
||||||
|
@PostMapping(value = "/pdf/csv", consumes = "multipart/form-data")
|
||||||
|
@Operation(
|
||||||
|
summary = "Extracts a PDF document to csv",
|
||||||
|
description =
|
||||||
|
"This operation takes an input PDF file and returns CSV file of whole page. Input:PDF Output:CSV Type:SISO")
|
||||||
|
public ResponseEntity<String> PdfToCsv(@ModelAttribute PDFFilePage form) throws Exception {
|
||||||
|
|
||||||
|
ArrayList<String> tableData = new ArrayList<>();
|
||||||
|
int columnsCount = 0;
|
||||||
|
|
||||||
|
try (PDDocument document =
|
||||||
|
PDDocument.load(new ByteArrayInputStream(form.getFileInput().getBytes()))) {
|
||||||
|
final double res = 72; // PDF units are at 72 DPI
|
||||||
|
PDFTableStripper stripper = new PDFTableStripper();
|
||||||
|
PDPage pdPage = document.getPage(form.getPageId() - 1);
|
||||||
|
stripper.extractTable(pdPage);
|
||||||
|
columnsCount = stripper.getColumns();
|
||||||
|
for (int c = 0; c < columnsCount; ++c) {
|
||||||
|
for (int r = 0; r < stripper.getRows(); ++r) {
|
||||||
|
tableData.add(stripper.getText(r, c));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ArrayList<String> notEmptyColumns = new ArrayList<>();
|
||||||
|
|
||||||
|
for (String item : tableData) {
|
||||||
|
if (!item.trim().isEmpty()) {
|
||||||
|
notEmptyColumns.add(item);
|
||||||
|
} else {
|
||||||
|
columnsCount--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> fullTable =
|
||||||
|
notEmptyColumns.stream()
|
||||||
|
.map(
|
||||||
|
(entity) ->
|
||||||
|
entity.replace('\n', ' ')
|
||||||
|
.replace('\r', ' ')
|
||||||
|
.trim()
|
||||||
|
.replaceAll("\\s{2,}", "|"))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
int rowsCount = fullTable.get(0).split("\\|").length;
|
||||||
|
|
||||||
|
ArrayList<String> headersList = getTableHeaders(columnsCount, fullTable);
|
||||||
|
ArrayList<String> recordList = getRecordsList(rowsCount, fullTable);
|
||||||
|
|
||||||
|
if (headersList.size() == 0 && recordList.size() == 0) {
|
||||||
|
throw new Exception("No table detected, no headers or records found");
|
||||||
|
}
|
||||||
|
|
||||||
|
StringWriter writer = new StringWriter();
|
||||||
|
try (CSVWriter csvWriter = new CSVWriter(writer)) {
|
||||||
|
csvWriter.writeNext(headersList.toArray(new String[0]));
|
||||||
|
for (String record : recordList) {
|
||||||
|
csvWriter.writeNext(record.split("\\|"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
headers.setContentDisposition(
|
||||||
|
ContentDisposition.builder("attachment")
|
||||||
|
.filename(
|
||||||
|
form.getFileInput()
|
||||||
|
.getOriginalFilename()
|
||||||
|
.replaceFirst("[.][^.]+$", "")
|
||||||
|
+ "_extracted.csv")
|
||||||
|
.build());
|
||||||
|
headers.setContentType(MediaType.parseMediaType("text/csv"));
|
||||||
|
|
||||||
|
return ResponseEntity.ok().headers(headers).body(writer.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private ArrayList<String> getRecordsList(int rowsCounts, List<String> items) {
|
||||||
|
ArrayList<String> recordsList = new ArrayList<>();
|
||||||
|
|
||||||
|
for (int b = 1; b < rowsCounts; b++) {
|
||||||
|
StringBuilder strbldr = new StringBuilder();
|
||||||
|
|
||||||
|
for (int i = 0; i < items.size(); i++) {
|
||||||
|
String[] parts = items.get(i).split("\\|");
|
||||||
|
strbldr.append(parts[b]);
|
||||||
|
if (i != items.size() - 1) {
|
||||||
|
strbldr.append("|");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
recordsList.add(strbldr.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
return recordsList;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ArrayList<String> getTableHeaders(int columnsCount, List<String> items) {
|
||||||
|
ArrayList<String> resultList = new ArrayList<>();
|
||||||
|
for (int i = 0; i < columnsCount; i++) {
|
||||||
|
String[] parts = items.get(i).split("\\|");
|
||||||
|
resultList.add(parts[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return resultList;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,210 @@
|
|||||||
|
package stirling.software.SPDF.controller.api.filters;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
|
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.model.api.PDFComparisonAndCount;
|
||||||
|
import stirling.software.SPDF.model.api.PDFWithPageNums;
|
||||||
|
import stirling.software.SPDF.model.api.filter.ContainsTextRequest;
|
||||||
|
import stirling.software.SPDF.model.api.filter.FileSizeRequest;
|
||||||
|
import stirling.software.SPDF.model.api.filter.PageRotationRequest;
|
||||||
|
import stirling.software.SPDF.model.api.filter.PageSizeRequest;
|
||||||
|
import stirling.software.SPDF.utils.PdfUtils;
|
||||||
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/filter")
|
||||||
|
@Tag(name = "Filter", description = "Filter APIs")
|
||||||
|
public class FilterController {
|
||||||
|
|
||||||
|
@PostMapping(consumes = "multipart/form-data", value = "/filter-contains-text")
|
||||||
|
@Operation(
|
||||||
|
summary = "Checks if a PDF contains set text, returns true if does",
|
||||||
|
description = "Input:PDF Output:Boolean Type:SISO")
|
||||||
|
public ResponseEntity<byte[]> containsText(@ModelAttribute ContainsTextRequest request)
|
||||||
|
throws IOException, InterruptedException {
|
||||||
|
MultipartFile inputFile = request.getFileInput();
|
||||||
|
String text = request.getText();
|
||||||
|
String pageNumber = request.getPageNumbers();
|
||||||
|
|
||||||
|
PDDocument pdfDocument = PDDocument.load(inputFile.getInputStream());
|
||||||
|
if (PdfUtils.hasText(pdfDocument, pageNumber, text))
|
||||||
|
return WebResponseUtils.pdfDocToWebResponse(
|
||||||
|
pdfDocument, inputFile.getOriginalFilename());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
@PostMapping(consumes = "multipart/form-data", value = "/filter-contains-image")
|
||||||
|
@Operation(
|
||||||
|
summary = "Checks if a PDF contains an image",
|
||||||
|
description = "Input:PDF Output:Boolean Type:SISO")
|
||||||
|
public ResponseEntity<byte[]> containsImage(@ModelAttribute PDFWithPageNums request)
|
||||||
|
throws IOException, InterruptedException {
|
||||||
|
MultipartFile inputFile = request.getFileInput();
|
||||||
|
String pageNumber = request.getPageNumbers();
|
||||||
|
|
||||||
|
PDDocument pdfDocument = PDDocument.load(inputFile.getInputStream());
|
||||||
|
if (PdfUtils.hasImages(pdfDocument, pageNumber))
|
||||||
|
return WebResponseUtils.pdfDocToWebResponse(
|
||||||
|
pdfDocument, inputFile.getOriginalFilename());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping(consumes = "multipart/form-data", value = "/filter-page-count")
|
||||||
|
@Operation(
|
||||||
|
summary = "Checks if a PDF is greater, less or equal to a setPageCount",
|
||||||
|
description = "Input:PDF Output:Boolean Type:SISO")
|
||||||
|
public ResponseEntity<byte[]> pageCount(@ModelAttribute PDFComparisonAndCount request)
|
||||||
|
throws IOException, InterruptedException {
|
||||||
|
MultipartFile inputFile = request.getFileInput();
|
||||||
|
String pageCount = request.getPageCount();
|
||||||
|
String comparator = request.getComparator();
|
||||||
|
// Load the PDF
|
||||||
|
PDDocument document = PDDocument.load(inputFile.getInputStream());
|
||||||
|
int actualPageCount = document.getNumberOfPages();
|
||||||
|
|
||||||
|
boolean valid = false;
|
||||||
|
// Perform the comparison
|
||||||
|
switch (comparator) {
|
||||||
|
case "Greater":
|
||||||
|
valid = actualPageCount > Integer.parseInt(pageCount);
|
||||||
|
break;
|
||||||
|
case "Equal":
|
||||||
|
valid = actualPageCount == Integer.parseInt(pageCount);
|
||||||
|
break;
|
||||||
|
case "Less":
|
||||||
|
valid = actualPageCount < Integer.parseInt(pageCount);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException("Invalid comparator: " + comparator);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (valid) return WebResponseUtils.multiPartFileToWebResponse(inputFile);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping(consumes = "multipart/form-data", value = "/filter-page-size")
|
||||||
|
@Operation(
|
||||||
|
summary = "Checks if a PDF is of a certain size",
|
||||||
|
description = "Input:PDF Output:Boolean Type:SISO")
|
||||||
|
public ResponseEntity<byte[]> pageSize(@ModelAttribute PageSizeRequest request)
|
||||||
|
throws IOException, InterruptedException {
|
||||||
|
MultipartFile inputFile = request.getFileInput();
|
||||||
|
String standardPageSize = request.getStandardPageSize();
|
||||||
|
String comparator = request.getComparator();
|
||||||
|
|
||||||
|
// Load the PDF
|
||||||
|
PDDocument document = PDDocument.load(inputFile.getInputStream());
|
||||||
|
|
||||||
|
PDPage firstPage = document.getPage(0);
|
||||||
|
PDRectangle actualPageSize = firstPage.getMediaBox();
|
||||||
|
|
||||||
|
// Calculate the area of the actual page size
|
||||||
|
float actualArea = actualPageSize.getWidth() * actualPageSize.getHeight();
|
||||||
|
|
||||||
|
// Get the standard size and calculate its area
|
||||||
|
PDRectangle standardSize = PdfUtils.textToPageSize(standardPageSize);
|
||||||
|
float standardArea = standardSize.getWidth() * standardSize.getHeight();
|
||||||
|
|
||||||
|
boolean valid = false;
|
||||||
|
// Perform the comparison
|
||||||
|
switch (comparator) {
|
||||||
|
case "Greater":
|
||||||
|
valid = actualArea > standardArea;
|
||||||
|
break;
|
||||||
|
case "Equal":
|
||||||
|
valid = actualArea == standardArea;
|
||||||
|
break;
|
||||||
|
case "Less":
|
||||||
|
valid = actualArea < standardArea;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException("Invalid comparator: " + comparator);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (valid) return WebResponseUtils.multiPartFileToWebResponse(inputFile);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping(consumes = "multipart/form-data", value = "/filter-file-size")
|
||||||
|
@Operation(
|
||||||
|
summary = "Checks if a PDF is a set file size",
|
||||||
|
description = "Input:PDF Output:Boolean Type:SISO")
|
||||||
|
public ResponseEntity<byte[]> fileSize(@ModelAttribute FileSizeRequest request)
|
||||||
|
throws IOException, InterruptedException {
|
||||||
|
MultipartFile inputFile = request.getFileInput();
|
||||||
|
String fileSize = request.getFileSize();
|
||||||
|
String comparator = request.getComparator();
|
||||||
|
|
||||||
|
// Get the file size
|
||||||
|
long actualFileSize = inputFile.getSize();
|
||||||
|
|
||||||
|
boolean valid = false;
|
||||||
|
// Perform the comparison
|
||||||
|
switch (comparator) {
|
||||||
|
case "Greater":
|
||||||
|
valid = actualFileSize > Long.parseLong(fileSize);
|
||||||
|
break;
|
||||||
|
case "Equal":
|
||||||
|
valid = actualFileSize == Long.parseLong(fileSize);
|
||||||
|
break;
|
||||||
|
case "Less":
|
||||||
|
valid = actualFileSize < Long.parseLong(fileSize);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException("Invalid comparator: " + comparator);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (valid) return WebResponseUtils.multiPartFileToWebResponse(inputFile);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping(consumes = "multipart/form-data", value = "/filter-page-rotation")
|
||||||
|
@Operation(
|
||||||
|
summary = "Checks if a PDF is of a certain rotation",
|
||||||
|
description = "Input:PDF Output:Boolean Type:SISO")
|
||||||
|
public ResponseEntity<byte[]> pageRotation(@ModelAttribute PageRotationRequest request)
|
||||||
|
throws IOException, InterruptedException {
|
||||||
|
MultipartFile inputFile = request.getFileInput();
|
||||||
|
int rotation = request.getRotation();
|
||||||
|
String comparator = request.getComparator();
|
||||||
|
|
||||||
|
// Load the PDF
|
||||||
|
PDDocument document = PDDocument.load(inputFile.getInputStream());
|
||||||
|
|
||||||
|
// Get the rotation of the first page
|
||||||
|
PDPage firstPage = document.getPage(0);
|
||||||
|
int actualRotation = firstPage.getRotation();
|
||||||
|
boolean valid = false;
|
||||||
|
// Perform the comparison
|
||||||
|
switch (comparator) {
|
||||||
|
case "Greater":
|
||||||
|
valid = actualRotation > rotation;
|
||||||
|
break;
|
||||||
|
case "Equal":
|
||||||
|
valid = actualRotation == rotation;
|
||||||
|
break;
|
||||||
|
case "Less":
|
||||||
|
valid = actualRotation < rotation;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException("Invalid comparator: " + comparator);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (valid) return WebResponseUtils.multiPartFileToWebResponse(inputFile);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
package stirling.software.SPDF.controller.api.misc;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
|
import org.apache.pdfbox.text.PDFTextStripper;
|
||||||
|
import org.apache.pdfbox.text.TextPosition;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.model.api.misc.ExtractHeaderRequest;
|
||||||
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/misc")
|
||||||
|
@Tag(name = "Misc", description = "Miscellaneous APIs")
|
||||||
|
public class AutoRenameController {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(AutoRenameController.class);
|
||||||
|
|
||||||
|
private static final float TITLE_FONT_SIZE_THRESHOLD = 20.0f;
|
||||||
|
private static final int LINE_LIMIT = 11;
|
||||||
|
|
||||||
|
@PostMapping(consumes = "multipart/form-data", value = "/auto-rename")
|
||||||
|
@Operation(
|
||||||
|
summary = "Extract header from PDF file",
|
||||||
|
description =
|
||||||
|
"This endpoint accepts a PDF file and attempts to extract its title or header based on heuristics. Input:PDF Output:PDF Type:SISO")
|
||||||
|
public ResponseEntity<byte[]> extractHeader(@ModelAttribute ExtractHeaderRequest request)
|
||||||
|
throws Exception {
|
||||||
|
MultipartFile file = request.getFileInput();
|
||||||
|
Boolean useFirstTextAsFallback = request.isUseFirstTextAsFallback();
|
||||||
|
|
||||||
|
PDDocument document = PDDocument.load(file.getInputStream());
|
||||||
|
PDFTextStripper reader =
|
||||||
|
new PDFTextStripper() {
|
||||||
|
class LineInfo {
|
||||||
|
String text;
|
||||||
|
float fontSize;
|
||||||
|
|
||||||
|
LineInfo(String text, float fontSize) {
|
||||||
|
this.text = text;
|
||||||
|
this.fontSize = fontSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<LineInfo> lineInfos = new ArrayList<>();
|
||||||
|
StringBuilder lineBuilder = new StringBuilder();
|
||||||
|
float lastY = -1;
|
||||||
|
float maxFontSizeInLine = 0.0f;
|
||||||
|
int lineCount = 0;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void processTextPosition(TextPosition text) {
|
||||||
|
if (lastY != text.getY() && lineCount < LINE_LIMIT) {
|
||||||
|
processLine();
|
||||||
|
lineBuilder = new StringBuilder(text.getUnicode());
|
||||||
|
maxFontSizeInLine = text.getFontSizeInPt();
|
||||||
|
lastY = text.getY();
|
||||||
|
lineCount++;
|
||||||
|
} else if (lineCount < LINE_LIMIT) {
|
||||||
|
lineBuilder.append(text.getUnicode());
|
||||||
|
if (text.getFontSizeInPt() > maxFontSizeInLine) {
|
||||||
|
maxFontSizeInLine = text.getFontSizeInPt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processLine() {
|
||||||
|
if (lineBuilder.length() > 0 && lineCount < LINE_LIMIT) {
|
||||||
|
lineInfos.add(new LineInfo(lineBuilder.toString(), maxFontSizeInLine));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getText(PDDocument doc) throws IOException {
|
||||||
|
this.lineInfos.clear();
|
||||||
|
this.lineBuilder = new StringBuilder();
|
||||||
|
this.lastY = -1;
|
||||||
|
this.maxFontSizeInLine = 0.0f;
|
||||||
|
this.lineCount = 0;
|
||||||
|
super.getText(doc);
|
||||||
|
processLine(); // Process the last line
|
||||||
|
|
||||||
|
// Merge lines with same font size
|
||||||
|
List<LineInfo> mergedLineInfos = new ArrayList<>();
|
||||||
|
for (int i = 0; i < lineInfos.size(); i++) {
|
||||||
|
String mergedText = lineInfos.get(i).text;
|
||||||
|
float fontSize = lineInfos.get(i).fontSize;
|
||||||
|
while (i + 1 < lineInfos.size()
|
||||||
|
&& lineInfos.get(i + 1).fontSize == fontSize) {
|
||||||
|
mergedText += " " + lineInfos.get(i + 1).text;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
mergedLineInfos.add(new LineInfo(mergedText, fontSize));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort lines by font size in descending order and get the first one
|
||||||
|
mergedLineInfos.sort(
|
||||||
|
Comparator.comparing((LineInfo li) -> li.fontSize).reversed());
|
||||||
|
String title =
|
||||||
|
mergedLineInfos.isEmpty() ? null : mergedLineInfos.get(0).text;
|
||||||
|
|
||||||
|
return title != null
|
||||||
|
? title
|
||||||
|
: (useFirstTextAsFallback
|
||||||
|
? (mergedLineInfos.isEmpty()
|
||||||
|
? null
|
||||||
|
: mergedLineInfos.get(mergedLineInfos.size() - 1)
|
||||||
|
.text)
|
||||||
|
: null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
String header = reader.getText(document);
|
||||||
|
|
||||||
|
// Sanitize the header string by removing characters not allowed in a filename.
|
||||||
|
if (header != null && header.length() < 255) {
|
||||||
|
header = header.replaceAll("[/\\\\?%*:|\"<>]", "");
|
||||||
|
return WebResponseUtils.pdfDocToWebResponse(document, header + ".pdf");
|
||||||
|
} else {
|
||||||
|
logger.info("File has no good title to be found");
|
||||||
|
return WebResponseUtils.pdfDocToWebResponse(document, file.getOriginalFilename());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
package stirling.software.SPDF.controller.api.misc;
|
||||||
|
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.awt.image.DataBufferByte;
|
||||||
|
import java.awt.image.DataBufferInt;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
import java.util.zip.ZipOutputStream;
|
||||||
|
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
|
import org.apache.pdfbox.rendering.PDFRenderer;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import com.google.zxing.BinaryBitmap;
|
||||||
|
import com.google.zxing.LuminanceSource;
|
||||||
|
import com.google.zxing.MultiFormatReader;
|
||||||
|
import com.google.zxing.NotFoundException;
|
||||||
|
import com.google.zxing.PlanarYUVLuminanceSource;
|
||||||
|
import com.google.zxing.Result;
|
||||||
|
import com.google.zxing.common.HybridBinarizer;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.model.api.misc.AutoSplitPdfRequest;
|
||||||
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/misc")
|
||||||
|
@Tag(name = "Misc", description = "Miscellaneous APIs")
|
||||||
|
public class AutoSplitPdfController {
|
||||||
|
|
||||||
|
private static final String QR_CONTENT = "https://github.com/Frooodle/Stirling-PDF";
|
||||||
|
|
||||||
|
@PostMapping(value = "/auto-split-pdf", consumes = "multipart/form-data")
|
||||||
|
@Operation(
|
||||||
|
summary = "Auto split PDF pages into separate documents",
|
||||||
|
description =
|
||||||
|
"This endpoint accepts a PDF file, scans each page for a specific QR code, and splits the document at the QR code boundaries. The output is a zip file containing each separate PDF document. Input:PDF Output:ZIP-PDF Type:SISO")
|
||||||
|
public ResponseEntity<byte[]> autoSplitPdf(@ModelAttribute AutoSplitPdfRequest request)
|
||||||
|
throws IOException {
|
||||||
|
MultipartFile file = request.getFileInput();
|
||||||
|
boolean duplexMode = request.isDuplexMode();
|
||||||
|
|
||||||
|
InputStream inputStream = file.getInputStream();
|
||||||
|
PDDocument document = PDDocument.load(inputStream);
|
||||||
|
PDFRenderer pdfRenderer = new PDFRenderer(document);
|
||||||
|
|
||||||
|
List<PDDocument> splitDocuments = new ArrayList<>();
|
||||||
|
List<ByteArrayOutputStream> splitDocumentsBoas = new ArrayList<>();
|
||||||
|
|
||||||
|
for (int page = 0; page < document.getNumberOfPages(); ++page) {
|
||||||
|
BufferedImage bim = pdfRenderer.renderImageWithDPI(page, 150);
|
||||||
|
String result = decodeQRCode(bim);
|
||||||
|
|
||||||
|
if (QR_CONTENT.equals(result) && page != 0) {
|
||||||
|
splitDocuments.add(new PDDocument());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!splitDocuments.isEmpty() && !QR_CONTENT.equals(result)) {
|
||||||
|
splitDocuments.get(splitDocuments.size() - 1).addPage(document.getPage(page));
|
||||||
|
} else if (page == 0) {
|
||||||
|
PDDocument firstDocument = new PDDocument();
|
||||||
|
firstDocument.addPage(document.getPage(page));
|
||||||
|
splitDocuments.add(firstDocument);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If duplexMode is true and current page is a divider, then skip next page
|
||||||
|
if (duplexMode && QR_CONTENT.equals(result)) {
|
||||||
|
page++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove split documents that have no pages
|
||||||
|
splitDocuments.removeIf(pdDocument -> pdDocument.getNumberOfPages() == 0);
|
||||||
|
|
||||||
|
for (PDDocument splitDocument : splitDocuments) {
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
splitDocument.save(baos);
|
||||||
|
splitDocumentsBoas.add(baos);
|
||||||
|
splitDocument.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
document.close();
|
||||||
|
|
||||||
|
Path zipFile = Files.createTempFile("split_documents", ".zip");
|
||||||
|
String filename = file.getOriginalFilename().replaceFirst("[.][^.]+$", "");
|
||||||
|
byte[] data;
|
||||||
|
|
||||||
|
try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(zipFile))) {
|
||||||
|
for (int i = 0; i < splitDocumentsBoas.size(); i++) {
|
||||||
|
String fileName = filename + "_" + (i + 1) + ".pdf";
|
||||||
|
ByteArrayOutputStream baos = splitDocumentsBoas.get(i);
|
||||||
|
byte[] pdf = baos.toByteArray();
|
||||||
|
|
||||||
|
ZipEntry pdfEntry = new ZipEntry(fileName);
|
||||||
|
zipOut.putNextEntry(pdfEntry);
|
||||||
|
zipOut.write(pdf);
|
||||||
|
zipOut.closeEntry();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
} finally {
|
||||||
|
data = Files.readAllBytes(zipFile);
|
||||||
|
Files.delete(zipFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
return WebResponseUtils.bytesToWebResponse(
|
||||||
|
data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String decodeQRCode(BufferedImage bufferedImage) {
|
||||||
|
LuminanceSource source;
|
||||||
|
|
||||||
|
if (bufferedImage.getRaster().getDataBuffer() instanceof DataBufferByte) {
|
||||||
|
byte[] pixels = ((DataBufferByte) bufferedImage.getRaster().getDataBuffer()).getData();
|
||||||
|
source =
|
||||||
|
new PlanarYUVLuminanceSource(
|
||||||
|
pixels,
|
||||||
|
bufferedImage.getWidth(),
|
||||||
|
bufferedImage.getHeight(),
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
bufferedImage.getWidth(),
|
||||||
|
bufferedImage.getHeight(),
|
||||||
|
false);
|
||||||
|
} else if (bufferedImage.getRaster().getDataBuffer() instanceof DataBufferInt) {
|
||||||
|
int[] pixels = ((DataBufferInt) bufferedImage.getRaster().getDataBuffer()).getData();
|
||||||
|
byte[] newPixels = new byte[pixels.length];
|
||||||
|
for (int i = 0; i < pixels.length; i++) {
|
||||||
|
newPixels[i] = (byte) (pixels[i] & 0xff);
|
||||||
|
}
|
||||||
|
source =
|
||||||
|
new PlanarYUVLuminanceSource(
|
||||||
|
newPixels,
|
||||||
|
bufferedImage.getWidth(),
|
||||||
|
bufferedImage.getHeight(),
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
bufferedImage.getWidth(),
|
||||||
|
bufferedImage.getHeight(),
|
||||||
|
false);
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"BufferedImage must have 8-bit gray scale, 24-bit RGB, 32-bit ARGB (packed int), byte gray, or 3-byte/4-byte RGB image data");
|
||||||
|
}
|
||||||
|
|
||||||
|
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
|
||||||
|
|
||||||
|
try {
|
||||||
|
Result result = new MultiFormatReader().decode(bitmap);
|
||||||
|
return result.getText();
|
||||||
|
} catch (NotFoundException e) {
|
||||||
|
return null; // there is no QR code in the image
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package stirling.software.SPDF.controller.api.other;
|
package stirling.software.SPDF.controller.api.misc;
|
||||||
|
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@@ -20,38 +20,38 @@ import org.apache.pdfbox.rendering.PDFRenderer;
|
|||||||
import org.apache.pdfbox.text.PDFTextStripper;
|
import org.apache.pdfbox.text.PDFTextStripper;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.ResponseEntity;
|
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.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
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.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 stirling.software.SPDF.utils.ImageFinder;
|
|
||||||
|
import stirling.software.SPDF.model.api.misc.RemoveBlankPagesRequest;
|
||||||
import stirling.software.SPDF.utils.PdfUtils;
|
import stirling.software.SPDF.utils.PdfUtils;
|
||||||
import stirling.software.SPDF.utils.ProcessExecutor;
|
import stirling.software.SPDF.utils.ProcessExecutor;
|
||||||
|
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
|
||||||
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/misc")
|
||||||
|
@Tag(name = "Misc", description = "Miscellaneous APIs")
|
||||||
public class BlankPageController {
|
public class BlankPageController {
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/remove-blanks")
|
@PostMapping(consumes = "multipart/form-data", value = "/remove-blanks")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Remove blank pages from a PDF file",
|
summary = "Remove blank pages from a PDF file",
|
||||||
description = "This endpoint removes blank pages from a given PDF file. Users can specify the threshold and white percentage to tune the detection of blank pages."
|
description =
|
||||||
)
|
"This endpoint removes blank pages from a given PDF file. Users can specify the threshold and white percentage to tune the detection of blank pages. Input:PDF Output:PDF Type:SISO")
|
||||||
public ResponseEntity<byte[]> removeBlankPages(
|
public ResponseEntity<byte[]> removeBlankPages(@ModelAttribute RemoveBlankPagesRequest request)
|
||||||
@RequestPart(required = true, value = "fileInput")
|
throws IOException, InterruptedException {
|
||||||
@Parameter(description = "The input PDF file from which blank pages will be removed", required = true)
|
MultipartFile inputFile = request.getFileInput();
|
||||||
MultipartFile inputFile,
|
int threshold = request.getThreshold();
|
||||||
@RequestParam(defaultValue = "10", name = "threshold")
|
float whitePercent = request.getWhitePercent();
|
||||||
@Parameter(description = "The threshold value to determine blank pages", example = "10")
|
|
||||||
int threshold,
|
PDDocument document = null;
|
||||||
@RequestParam(defaultValue = "99.9", name = "whitePercent")
|
|
||||||
@Parameter(description = "The percentage of white color on a page to consider it as blank", example = "99.9")
|
|
||||||
float whitePercent) throws IOException, InterruptedException {
|
|
||||||
|
|
||||||
PDDocument document = null;
|
|
||||||
try {
|
try {
|
||||||
document = PDDocument.load(inputFile.getInputStream());
|
document = PDDocument.load(inputFile.getInputStream());
|
||||||
PDPageTree pages = document.getDocumentCatalog().getPages();
|
PDPageTree pages = document.getDocumentCatalog().getPages();
|
||||||
@@ -71,24 +71,37 @@ public class BlankPageController {
|
|||||||
pagesToKeepIndex.add(pageIndex);
|
pagesToKeepIndex.add(pageIndex);
|
||||||
System.out.println("page " + pageIndex + " has text");
|
System.out.println("page " + pageIndex + " has text");
|
||||||
} else {
|
} else {
|
||||||
boolean hasImages = hasImagesOnPage(page);
|
boolean hasImages = PdfUtils.hasImagesOnPage(page);
|
||||||
if (hasImages) {
|
if (hasImages) {
|
||||||
System.out.println("page " + pageIndex + " has image");
|
System.out.println("page " + pageIndex + " has image");
|
||||||
|
|
||||||
Path tempFile = Files.createTempFile("image_", ".png");
|
Path tempFile = Files.createTempFile("image_", ".png");
|
||||||
|
|
||||||
// Render image and save as temp file
|
// Render image and save as temp file
|
||||||
BufferedImage image = pdfRenderer.renderImageWithDPI(pageIndex, 300);
|
BufferedImage image = pdfRenderer.renderImageWithDPI(pageIndex, 300);
|
||||||
ImageIO.write(image, "png", tempFile.toFile());
|
ImageIO.write(image, "png", tempFile.toFile());
|
||||||
|
|
||||||
List<String> command = new ArrayList<>(Arrays.asList("python3", System.getProperty("user.dir") + "/scripts/detect-blank-pages.py", tempFile.toString() ,"--threshold", String.valueOf(threshold), "--white_percent", String.valueOf(whitePercent)));
|
List<String> command =
|
||||||
|
new ArrayList<>(
|
||||||
|
Arrays.asList(
|
||||||
|
"python3",
|
||||||
|
System.getProperty("user.dir")
|
||||||
|
+ "/scripts/detect-blank-pages.py",
|
||||||
|
tempFile.toString(),
|
||||||
|
"--threshold",
|
||||||
|
String.valueOf(threshold),
|
||||||
|
"--white_percent",
|
||||||
|
String.valueOf(whitePercent)));
|
||||||
|
|
||||||
// Run CLI command
|
// Run CLI command
|
||||||
int returnCode = ProcessExecutor.getInstance(ProcessExecutor.Processes.PYTHON_OPENCV).runCommandWithOutputHandling(command);
|
ProcessExecutorResult returnCode =
|
||||||
|
ProcessExecutor.getInstance(ProcessExecutor.Processes.PYTHON_OPENCV)
|
||||||
|
.runCommandWithOutputHandling(command);
|
||||||
|
|
||||||
// does contain data
|
// does contain data
|
||||||
if (returnCode == 0) {
|
if (returnCode.getRc() == 0) {
|
||||||
System.out.println("page " + pageIndex + " has image which is not blank");
|
System.out.println(
|
||||||
|
"page " + pageIndex + " has image which is not blank");
|
||||||
pagesToKeepIndex.add(pageIndex);
|
pagesToKeepIndex.add(pageIndex);
|
||||||
} else {
|
} else {
|
||||||
System.out.println("Skipping, Image was blank for page #" + pageIndex);
|
System.out.println("Skipping, Image was blank for page #" + pageIndex);
|
||||||
@@ -96,12 +109,12 @@ public class BlankPageController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
pageIndex++;
|
pageIndex++;
|
||||||
|
|
||||||
}
|
}
|
||||||
System.out.print("pagesToKeep=" + pagesToKeepIndex.size());
|
System.out.print("pagesToKeep=" + pagesToKeepIndex.size());
|
||||||
|
|
||||||
// Remove pages not present in pagesToKeepIndex
|
// Remove pages not present in pagesToKeepIndex
|
||||||
List<Integer> pageIndices = IntStream.range(0, pages.getCount()).boxed().collect(Collectors.toList());
|
List<Integer> pageIndices =
|
||||||
|
IntStream.range(0, pages.getCount()).boxed().collect(Collectors.toList());
|
||||||
Collections.reverse(pageIndices); // Reverse to prevent index shifting during removal
|
Collections.reverse(pageIndices); // Reverse to prevent index shifting during removal
|
||||||
for (Integer i : pageIndices) {
|
for (Integer i : pageIndices) {
|
||||||
if (!pagesToKeepIndex.contains(i)) {
|
if (!pagesToKeepIndex.contains(i)) {
|
||||||
@@ -109,20 +122,15 @@ public class BlankPageController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return PdfUtils.pdfDocToWebResponse(document, inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_blanksRemoved.pdf");
|
return WebResponseUtils.pdfDocToWebResponse(
|
||||||
|
document,
|
||||||
|
inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "")
|
||||||
|
+ "_blanksRemoved.pdf");
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
|
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||||
} finally {
|
} finally {
|
||||||
if (document != null)
|
if (document != null) document.close();
|
||||||
document.close();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private static boolean hasImagesOnPage(PDPage page) throws IOException {
|
|
||||||
ImageFinder imageFinder = new ImageFinder(page);
|
|
||||||
imageFinder.processPage(page);
|
|
||||||
return imageFinder.hasImages();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package stirling.software.SPDF.controller.api.other;
|
package stirling.software.SPDF.controller.api.misc;
|
||||||
|
|
||||||
import java.awt.Image;
|
import java.awt.Image;
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
@@ -22,40 +22,47 @@ import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
|
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.RequestParam;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
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.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.media.Schema;
|
|
||||||
import stirling.software.SPDF.utils.PdfUtils;
|
import stirling.software.SPDF.model.api.misc.OptimizePdfRequest;
|
||||||
|
import stirling.software.SPDF.utils.GeneralUtils;
|
||||||
import stirling.software.SPDF.utils.ProcessExecutor;
|
import stirling.software.SPDF.utils.ProcessExecutor;
|
||||||
|
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
|
||||||
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/misc")
|
||||||
|
@Tag(name = "Misc", description = "Miscellaneous APIs")
|
||||||
public class CompressController {
|
public class CompressController {
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(CompressController.class);
|
private static final Logger logger = LoggerFactory.getLogger(CompressController.class);
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/compress-pdf")
|
@PostMapping(consumes = "multipart/form-data", value = "/compress-pdf")
|
||||||
@Operation(summary = "Optimize PDF file", description = "This endpoint accepts a PDF file and optimizes it based on the provided parameters.")
|
@Operation(
|
||||||
public ResponseEntity<byte[]> optimizePdf(
|
summary = "Optimize PDF file",
|
||||||
@RequestPart(value = "fileInput") @Parameter(description = "The input PDF file to be optimized.", required = true) MultipartFile inputFile,
|
description =
|
||||||
@RequestParam(required = false, value = "optimizeLevel") @Parameter(description = "The level of optimization to apply to the PDF file. Higher values indicate greater compression but may reduce quality.", schema = @Schema(allowableValues = {
|
"This endpoint accepts a PDF file and optimizes it based on the provided parameters. Input:PDF Output:PDF Type:SISO")
|
||||||
"1", "2", "3", "4", "5" })) Integer optimizeLevel,
|
public ResponseEntity<byte[]> optimizePdf(@ModelAttribute OptimizePdfRequest request)
|
||||||
@RequestParam(value = "expectedOutputSize", required = false) @Parameter(description = "The expected output size, e.g. '100MB', '25KB', etc.", required = false) String expectedOutputSizeString)
|
|
||||||
throws Exception {
|
throws Exception {
|
||||||
|
MultipartFile inputFile = request.getFileInput();
|
||||||
|
Integer optimizeLevel = request.getOptimizeLevel();
|
||||||
|
String expectedOutputSizeString = request.getExpectedOutputSize();
|
||||||
|
|
||||||
if(expectedOutputSizeString == null && optimizeLevel == null) {
|
if (expectedOutputSizeString == null && optimizeLevel == null) {
|
||||||
throw new Exception("Both expected output size and optimize level are not specified");
|
throw new Exception("Both expected output size and optimize level are not specified");
|
||||||
}
|
}
|
||||||
|
|
||||||
Long expectedOutputSize = 0L;
|
Long expectedOutputSize = 0L;
|
||||||
boolean autoMode = false;
|
boolean autoMode = false;
|
||||||
if (expectedOutputSizeString != null && expectedOutputSizeString.length() > 1 ) {
|
if (expectedOutputSizeString != null && expectedOutputSizeString.length() > 1) {
|
||||||
expectedOutputSize = PdfUtils.convertSizeToBytes(expectedOutputSizeString);
|
expectedOutputSize = GeneralUtils.convertSizeToBytes(expectedOutputSizeString);
|
||||||
autoMode = true;
|
autoMode = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,8 +75,9 @@ public class CompressController {
|
|||||||
// Prepare the output file path
|
// Prepare the output file path
|
||||||
Path tempOutputFile = Files.createTempFile("output_", ".pdf");
|
Path tempOutputFile = Files.createTempFile("output_", ".pdf");
|
||||||
|
|
||||||
// Determine initial optimization level based on expected size reduction, only if in autoMode
|
// Determine initial optimization level based on expected size reduction, only if in
|
||||||
if(autoMode) {
|
// autoMode
|
||||||
|
if (autoMode) {
|
||||||
double sizeReductionRatio = expectedOutputSize / (double) inputFileSize;
|
double sizeReductionRatio = expectedOutputSize / (double) inputFileSize;
|
||||||
if (sizeReductionRatio > 0.7) {
|
if (sizeReductionRatio > 0.7) {
|
||||||
optimizeLevel = 1;
|
optimizeLevel = 1;
|
||||||
@@ -91,20 +99,20 @@ public class CompressController {
|
|||||||
command.add("-dCompatibilityLevel=1.4");
|
command.add("-dCompatibilityLevel=1.4");
|
||||||
|
|
||||||
switch (optimizeLevel) {
|
switch (optimizeLevel) {
|
||||||
case 1:
|
case 1:
|
||||||
command.add("-dPDFSETTINGS=/prepress");
|
command.add("-dPDFSETTINGS=/prepress");
|
||||||
break;
|
break;
|
||||||
case 2:
|
case 2:
|
||||||
command.add("-dPDFSETTINGS=/printer");
|
command.add("-dPDFSETTINGS=/printer");
|
||||||
break;
|
break;
|
||||||
case 3:
|
case 3:
|
||||||
command.add("-dPDFSETTINGS=/ebook");
|
command.add("-dPDFSETTINGS=/ebook");
|
||||||
break;
|
break;
|
||||||
case 4:
|
case 4:
|
||||||
command.add("-dPDFSETTINGS=/screen");
|
command.add("-dPDFSETTINGS=/screen");
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
command.add("-dPDFSETTINGS=/default");
|
command.add("-dPDFSETTINGS=/default");
|
||||||
}
|
}
|
||||||
|
|
||||||
command.add("-dNOPAUSE");
|
command.add("-dNOPAUSE");
|
||||||
@@ -113,7 +121,9 @@ public class CompressController {
|
|||||||
command.add("-sOutputFile=" + tempOutputFile.toString());
|
command.add("-sOutputFile=" + tempOutputFile.toString());
|
||||||
command.add(tempInputFile.toString());
|
command.add(tempInputFile.toString());
|
||||||
|
|
||||||
int returnCode = ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT).runCommandWithOutputHandling(command);
|
ProcessExecutorResult returnCode =
|
||||||
|
ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT)
|
||||||
|
.runCommandWithOutputHandling(command);
|
||||||
|
|
||||||
// Check if file size is within expected size or not auto mode so instantly finish
|
// Check if file size is within expected size or not auto mode so instantly finish
|
||||||
long outputFileSize = Files.size(tempOutputFile);
|
long outputFileSize = Files.size(tempOutputFile);
|
||||||
@@ -122,19 +132,18 @@ public class CompressController {
|
|||||||
} else {
|
} else {
|
||||||
// Increase optimization level for next iteration
|
// Increase optimization level for next iteration
|
||||||
optimizeLevel++;
|
optimizeLevel++;
|
||||||
if(autoMode && optimizeLevel > 3) {
|
if (autoMode && optimizeLevel > 3) {
|
||||||
System.out.println("Skipping level 4 due to bad results in auto mode");
|
System.out.println("Skipping level 4 due to bad results in auto mode");
|
||||||
sizeMet = true;
|
sizeMet = true;
|
||||||
} else if(optimizeLevel == 5) {
|
} else if (optimizeLevel == 5) {
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
System.out.println("Increasing ghostscript optimisation level to " + optimizeLevel);
|
System.out.println(
|
||||||
|
"Increasing ghostscript optimisation level to " + optimizeLevel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (expectedOutputSize != null && autoMode) {
|
if (expectedOutputSize != null && autoMode) {
|
||||||
long outputFileSize = Files.size(tempOutputFile);
|
long outputFileSize = Files.size(tempOutputFile);
|
||||||
if (outputFileSize > expectedOutputSize) {
|
if (outputFileSize > expectedOutputSize) {
|
||||||
@@ -154,8 +163,8 @@ public class CompressController {
|
|||||||
BufferedImage bufferedImage = image.getImage();
|
BufferedImage bufferedImage = image.getImage();
|
||||||
|
|
||||||
// Calculate the new dimensions
|
// Calculate the new dimensions
|
||||||
int newWidth = (int)(bufferedImage.getWidth() * scaleFactor);
|
int newWidth = (int) (bufferedImage.getWidth() * scaleFactor);
|
||||||
int newHeight = (int)(bufferedImage.getHeight() * scaleFactor);
|
int newHeight = (int) (bufferedImage.getHeight() * scaleFactor);
|
||||||
|
|
||||||
// If the new dimensions are zero, skip this iteration
|
// If the new dimensions are zero, skip this iteration
|
||||||
if (newWidth == 0 || newHeight == 0) {
|
if (newWidth == 0 || newHeight == 0) {
|
||||||
@@ -163,23 +172,39 @@ public class CompressController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, proceed with the scaling
|
// Otherwise, proceed with the scaling
|
||||||
Image scaledImage = bufferedImage.getScaledInstance(newWidth, newHeight, Image.SCALE_SMOOTH);
|
Image scaledImage =
|
||||||
|
bufferedImage.getScaledInstance(
|
||||||
|
newWidth, newHeight, Image.SCALE_SMOOTH);
|
||||||
|
|
||||||
// Convert the scaled image back to a BufferedImage
|
// Convert the scaled image back to a BufferedImage
|
||||||
BufferedImage scaledBufferedImage = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_RGB);
|
BufferedImage scaledBufferedImage =
|
||||||
scaledBufferedImage.getGraphics().drawImage(scaledImage, 0, 0, null);
|
new BufferedImage(
|
||||||
|
newWidth,
|
||||||
|
newHeight,
|
||||||
|
BufferedImage.TYPE_INT_RGB);
|
||||||
|
scaledBufferedImage
|
||||||
|
.getGraphics()
|
||||||
|
.drawImage(scaledImage, 0, 0, null);
|
||||||
|
|
||||||
// Compress the scaled image
|
// Compress the scaled image
|
||||||
ByteArrayOutputStream compressedImageStream = new ByteArrayOutputStream();
|
ByteArrayOutputStream compressedImageStream =
|
||||||
ImageIO.write(scaledBufferedImage, "jpeg", compressedImageStream);
|
new ByteArrayOutputStream();
|
||||||
|
ImageIO.write(
|
||||||
|
scaledBufferedImage, "jpeg", compressedImageStream);
|
||||||
byte[] imageBytes = compressedImageStream.toByteArray();
|
byte[] imageBytes = compressedImageStream.toByteArray();
|
||||||
compressedImageStream.close();
|
compressedImageStream.close();
|
||||||
|
|
||||||
// Convert compressed image back to PDImageXObject
|
// Convert compressed image back to PDImageXObject
|
||||||
ByteArrayInputStream bais = new ByteArrayInputStream(imageBytes);
|
ByteArrayInputStream bais =
|
||||||
PDImageXObject compressedImage = PDImageXObject.createFromByteArray(doc, imageBytes, image.getCOSObject().toString());
|
new ByteArrayInputStream(imageBytes);
|
||||||
|
PDImageXObject compressedImage =
|
||||||
|
PDImageXObject.createFromByteArray(
|
||||||
|
doc,
|
||||||
|
imageBytes,
|
||||||
|
image.getCOSObject().toString());
|
||||||
|
|
||||||
// Replace the image in the resources with the compressed version
|
// Replace the image in the resources with the compressed
|
||||||
|
// version
|
||||||
res.put(name, compressedImage);
|
res.put(name, compressedImage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -191,16 +216,23 @@ public class CompressController {
|
|||||||
long currentSize = Files.size(tempOutputFile);
|
long currentSize = Files.size(tempOutputFile);
|
||||||
// Check if the overall PDF size is still larger than expectedOutputSize
|
// Check if the overall PDF size is still larger than expectedOutputSize
|
||||||
if (currentSize > expectedOutputSize) {
|
if (currentSize > expectedOutputSize) {
|
||||||
// Log the current file size and scaleFactor
|
// Log the current file size and scaleFactor
|
||||||
|
|
||||||
System.out.println("Current file size: " + FileUtils.byteCountToDisplaySize(currentSize));
|
System.out.println(
|
||||||
|
"Current file size: "
|
||||||
|
+ FileUtils.byteCountToDisplaySize(currentSize));
|
||||||
System.out.println("Current scale factor: " + scaleFactor);
|
System.out.println("Current scale factor: " + scaleFactor);
|
||||||
|
|
||||||
// The file is still too large, reduce scaleFactor and try again
|
// The file is still too large, reduce scaleFactor and try again
|
||||||
scaleFactor *= 0.9; // reduce scaleFactor by 10%
|
scaleFactor *= 0.9; // reduce scaleFactor by 10%
|
||||||
// Avoid scaleFactor being too small, causing the image to shrink to 0
|
// Avoid scaleFactor being too small, causing the image to shrink to 0
|
||||||
if(scaleFactor < 0.2 || previousFileSize == currentSize){
|
if (scaleFactor < 0.2 || previousFileSize == currentSize) {
|
||||||
throw new RuntimeException("Could not reach the desired size without excessively degrading image quality, lowest size recommended is " + FileUtils.byteCountToDisplaySize(currentSize) + ", " + currentSize + " bytes");
|
throw new RuntimeException(
|
||||||
|
"Could not reach the desired size without excessively degrading image quality, lowest size recommended is "
|
||||||
|
+ FileUtils.byteCountToDisplaySize(currentSize)
|
||||||
|
+ ", "
|
||||||
|
+ currentSize
|
||||||
|
+ " bytes");
|
||||||
}
|
}
|
||||||
previousFileSize = currentSize;
|
previousFileSize = currentSize;
|
||||||
} else {
|
} else {
|
||||||
@@ -208,23 +240,30 @@ public class CompressController {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read the optimized PDF file
|
// Read the optimized PDF file
|
||||||
byte[] pdfBytes = Files.readAllBytes(tempOutputFile);
|
byte[] pdfBytes = Files.readAllBytes(tempOutputFile);
|
||||||
|
|
||||||
|
// Check if optimized file is larger than the original
|
||||||
|
if (pdfBytes.length > inputFileSize) {
|
||||||
|
// Log the occurrence
|
||||||
|
logger.warn(
|
||||||
|
"Optimized file is larger than the original. Returning the original file instead.");
|
||||||
|
|
||||||
|
// Read the original file again
|
||||||
|
pdfBytes = Files.readAllBytes(tempInputFile);
|
||||||
|
}
|
||||||
|
|
||||||
// Clean up the temporary files
|
// Clean up the temporary files
|
||||||
Files.delete(tempInputFile);
|
Files.delete(tempInputFile);
|
||||||
Files.delete(tempOutputFile);
|
Files.delete(tempOutputFile);
|
||||||
|
|
||||||
// Return the optimized PDF as a response
|
// Return the optimized PDF as a response
|
||||||
String outputFilename = inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_Optimized.pdf";
|
String outputFilename =
|
||||||
return PdfUtils.bytesToWebResponse(pdfBytes, outputFilename);
|
inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_Optimized.pdf";
|
||||||
|
return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
package stirling.software.SPDF.controller.api.misc;
|
||||||
|
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.StandardCopyOption;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
import java.util.zip.ZipOutputStream;
|
||||||
|
|
||||||
|
import javax.imageio.ImageIO;
|
||||||
|
|
||||||
|
import org.apache.commons.io.FileUtils;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
|
import org.apache.pdfbox.rendering.PDFRenderer;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Content;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.model.api.misc.ExtractImageScansRequest;
|
||||||
|
import stirling.software.SPDF.utils.ProcessExecutor;
|
||||||
|
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
|
||||||
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/misc")
|
||||||
|
@Tag(name = "Misc", description = "Miscellaneous APIs")
|
||||||
|
public class ExtractImageScansController {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(ExtractImageScansController.class);
|
||||||
|
|
||||||
|
@PostMapping(consumes = "multipart/form-data", value = "/extract-image-scans")
|
||||||
|
@Operation(
|
||||||
|
summary = "Extract image scans from an input file",
|
||||||
|
description =
|
||||||
|
"This endpoint extracts image scans from a given file based on certain parameters. Users can specify angle threshold, tolerance, minimum area, minimum contour area, and border size. Input:PDF Output:IMAGE/ZIP Type:SIMO")
|
||||||
|
public ResponseEntity<byte[]> extractImageScans(
|
||||||
|
@RequestBody(
|
||||||
|
description = "Form data containing file and extraction parameters",
|
||||||
|
required = true,
|
||||||
|
content =
|
||||||
|
@Content(
|
||||||
|
mediaType = "multipart/form-data",
|
||||||
|
schema =
|
||||||
|
@Schema(
|
||||||
|
implementation =
|
||||||
|
ExtractImageScansRequest
|
||||||
|
.class) // This should
|
||||||
|
// represent
|
||||||
|
// your form's
|
||||||
|
// structure
|
||||||
|
))
|
||||||
|
ExtractImageScansRequest form)
|
||||||
|
throws IOException, InterruptedException {
|
||||||
|
String fileName = form.getFileInput().getOriginalFilename();
|
||||||
|
String extension = fileName.substring(fileName.lastIndexOf(".") + 1);
|
||||||
|
|
||||||
|
List<String> images = new ArrayList<>();
|
||||||
|
|
||||||
|
// Check if input file is a PDF
|
||||||
|
if (extension.equalsIgnoreCase("pdf")) {
|
||||||
|
// Load PDF document
|
||||||
|
try (PDDocument document =
|
||||||
|
PDDocument.load(new ByteArrayInputStream(form.getFileInput().getBytes()))) {
|
||||||
|
PDFRenderer pdfRenderer = new PDFRenderer(document);
|
||||||
|
int pageCount = document.getNumberOfPages();
|
||||||
|
images = new ArrayList<>();
|
||||||
|
|
||||||
|
// Create images of all pages
|
||||||
|
for (int i = 0; i < pageCount; i++) {
|
||||||
|
// Create temp file to save the image
|
||||||
|
Path tempFile = Files.createTempFile("image_", ".png");
|
||||||
|
|
||||||
|
// Render image and save as temp file
|
||||||
|
BufferedImage image = pdfRenderer.renderImageWithDPI(i, 300);
|
||||||
|
ImageIO.write(image, "png", tempFile.toFile());
|
||||||
|
|
||||||
|
// Add temp file path to images list
|
||||||
|
images.add(tempFile.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Path tempInputFile = Files.createTempFile("input_", "." + extension);
|
||||||
|
Files.copy(
|
||||||
|
form.getFileInput().getInputStream(),
|
||||||
|
tempInputFile,
|
||||||
|
StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
// Add input file path to images list
|
||||||
|
images.add(tempInputFile.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
List<byte[]> processedImageBytes = new ArrayList<>();
|
||||||
|
|
||||||
|
// Process each image
|
||||||
|
for (int i = 0; i < images.size(); i++) {
|
||||||
|
|
||||||
|
Path tempDir = Files.createTempDirectory("openCV_output");
|
||||||
|
List<String> command =
|
||||||
|
new ArrayList<>(
|
||||||
|
Arrays.asList(
|
||||||
|
"python3",
|
||||||
|
"./scripts/split_photos.py",
|
||||||
|
images.get(i),
|
||||||
|
tempDir.toString(),
|
||||||
|
"--angle_threshold",
|
||||||
|
String.valueOf(form.getAngleThreshold()),
|
||||||
|
"--tolerance",
|
||||||
|
String.valueOf(form.getTolerance()),
|
||||||
|
"--min_area",
|
||||||
|
String.valueOf(form.getMinArea()),
|
||||||
|
"--min_contour_area",
|
||||||
|
String.valueOf(form.getMinContourArea()),
|
||||||
|
"--border_size",
|
||||||
|
String.valueOf(form.getBorderSize())));
|
||||||
|
|
||||||
|
// Run CLI command
|
||||||
|
ProcessExecutorResult returnCode =
|
||||||
|
ProcessExecutor.getInstance(ProcessExecutor.Processes.PYTHON_OPENCV)
|
||||||
|
.runCommandWithOutputHandling(command);
|
||||||
|
|
||||||
|
// Read the output photos in temp directory
|
||||||
|
List<Path> tempOutputFiles = Files.list(tempDir).sorted().collect(Collectors.toList());
|
||||||
|
for (Path tempOutputFile : tempOutputFiles) {
|
||||||
|
byte[] imageBytes = Files.readAllBytes(tempOutputFile);
|
||||||
|
processedImageBytes.add(imageBytes);
|
||||||
|
}
|
||||||
|
// Clean up the temporary directory
|
||||||
|
FileUtils.deleteDirectory(tempDir.toFile());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create zip file if multiple images
|
||||||
|
if (processedImageBytes.size() > 1) {
|
||||||
|
String outputZipFilename = fileName.replaceFirst("[.][^.]+$", "") + "_processed.zip";
|
||||||
|
Path tempZipFile = Files.createTempFile("output_", ".zip");
|
||||||
|
|
||||||
|
try (ZipOutputStream zipOut =
|
||||||
|
new ZipOutputStream(new FileOutputStream(tempZipFile.toFile()))) {
|
||||||
|
// Add processed images to the zip
|
||||||
|
for (int i = 0; i < processedImageBytes.size(); i++) {
|
||||||
|
ZipEntry entry =
|
||||||
|
new ZipEntry(
|
||||||
|
fileName.replaceFirst("[.][^.]+$", "")
|
||||||
|
+ "_"
|
||||||
|
+ (i + 1)
|
||||||
|
+ ".png");
|
||||||
|
zipOut.putNextEntry(entry);
|
||||||
|
zipOut.write(processedImageBytes.get(i));
|
||||||
|
zipOut.closeEntry();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] zipBytes = Files.readAllBytes(tempZipFile);
|
||||||
|
|
||||||
|
// Clean up the temporary zip file
|
||||||
|
Files.delete(tempZipFile);
|
||||||
|
|
||||||
|
return WebResponseUtils.bytesToWebResponse(
|
||||||
|
zipBytes, outputZipFilename, MediaType.APPLICATION_OCTET_STREAM);
|
||||||
|
} else {
|
||||||
|
// Return the processed image as a response
|
||||||
|
byte[] imageBytes = processedImageBytes.get(0);
|
||||||
|
return WebResponseUtils.bytesToWebResponse(
|
||||||
|
imageBytes,
|
||||||
|
fileName.replaceFirst("[.][^.]+$", "") + ".png",
|
||||||
|
MediaType.IMAGE_PNG);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user