Compare commits
432 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0dd91f209a | ||
|
|
869e2dc62d | ||
|
|
a28f14b70e | ||
|
|
b5de6a73cc | ||
|
|
45e2623b9b | ||
|
|
6d95bfdee0 | ||
|
|
f9111e556c | ||
|
|
fa746a2b51 | ||
|
|
e43292f4dd | ||
|
|
c56c5f80ab | ||
|
|
f2eb5dd7d3 | ||
|
|
ffe221b93c | ||
|
|
3f252e29a1 | ||
|
|
7c0fd02126 | ||
|
|
7109dd7905 | ||
|
|
e30665e7c8 | ||
|
|
514606789e | ||
|
|
dd1a441f92 | ||
|
|
31c48aec90 | ||
|
|
6709e0c46d | ||
|
|
10dd5e4a40 | ||
|
|
6fc9a2032a | ||
|
|
ffec5f7b54 | ||
|
|
b904a46bca | ||
|
|
26a457f9d0 | ||
|
|
e9042e0b7e | ||
|
|
521dff737f | ||
|
|
2968a696cd | ||
|
|
e0d3bbf13b | ||
|
|
89e763d959 | ||
|
|
1f9a0ed0e3 | ||
|
|
f203e07f55 | ||
|
|
56d4c02445 | ||
|
|
c20936b485 | ||
|
|
389323c190 | ||
|
|
f0dd48b3b1 | ||
|
|
b860146c93 | ||
|
|
23b662e5dc | ||
|
|
7160ba47b1 | ||
|
|
bdea990b18 | ||
|
|
1cbc6307c3 | ||
|
|
1e42f54ec7 | ||
|
|
44e85a1d38 | ||
|
|
54073767af | ||
|
|
3fa41e058a | ||
|
|
44f9313012 | ||
|
|
dcf13e9ade | ||
|
|
811c19e00d | ||
|
|
f2b7aeeb1c | ||
|
|
840694c527 | ||
|
|
21e5002d73 | ||
|
|
36d6c06237 | ||
|
|
29ec42bc35 | ||
|
|
425502b3e3 | ||
|
|
692a526900 | ||
|
|
3a27d97811 | ||
|
|
af91c73e7a | ||
|
|
526a30d033 | ||
|
|
bf8d6d2337 | ||
|
|
5628300f51 | ||
|
|
72ba97a00c | ||
|
|
1634987171 | ||
|
|
0f43723250 | ||
|
|
34e2128a39 | ||
|
|
0a0887aafc | ||
|
|
7c0c33ca63 | ||
|
|
8b2f24affd | ||
|
|
cbe750c76c | ||
|
|
5f6d24f805 | ||
|
|
be5d5fdf04 | ||
|
|
a04dc605df | ||
|
|
503acc9408 | ||
|
|
9b166da57d | ||
|
|
66e566555e | ||
|
|
f7aebf22c8 | ||
|
|
bb0b5f0528 | ||
|
|
9e3d5a5bc5 | ||
|
|
9ba5c6d4be | ||
|
|
00f7fe7ac3 | ||
|
|
f4fcede771 | ||
|
|
b69646d00b | ||
|
|
9e81b161c3 | ||
|
|
66ce7511ca | ||
|
|
d17db24aa9 | ||
|
|
ac5273244c | ||
|
|
27113f99cb | ||
|
|
fa31a4e340 | ||
|
|
4d53119390 | ||
|
|
303b8e032b | ||
|
|
d6b1fec69d | ||
|
|
5c572a7d89 | ||
|
|
04d1ff3822 | ||
|
|
eb8a494b5c | ||
|
|
4dfac2f46f | ||
|
|
547f231e29 | ||
|
|
38979dd362 | ||
|
|
890163053b | ||
|
|
fbbc71d7e6 | ||
|
|
4372536c17 | ||
|
|
7f577a6052 | ||
|
|
d7afc574a6 | ||
|
|
c622ee915b | ||
|
|
d0df392eef | ||
|
|
1c33500815 | ||
|
|
d730c6a12f | ||
|
|
b71f6f93b1 | ||
|
|
32dd328048 | ||
|
|
d9fa8f7b48 | ||
|
|
777e512e61 | ||
|
|
e7e3b34b37 | ||
|
|
6ed9e1c707 | ||
|
|
318076254d | ||
|
|
4fea8d10f8 | ||
|
|
8c9d6f7b66 | ||
|
|
30444fc9bb | ||
|
|
70349d642b | ||
|
|
afaec64afd | ||
|
|
2a9fdff605 | ||
|
|
34c7ee46a0 | ||
|
|
5ee702f364 | ||
|
|
1b5d21a22e | ||
|
|
c3e5157dee | ||
|
|
6d859e4c25 | ||
|
|
95a9aca5b5 | ||
|
|
4c8f582c56 | ||
|
|
71e93e3cb5 | ||
|
|
6c052a7b25 | ||
|
|
1e0ec8345a | ||
|
|
eddcc11fe4 | ||
|
|
3189d9dda8 | ||
|
|
5185fd13b8 | ||
|
|
a5000fbbc5 | ||
|
|
e74a8e434b | ||
|
|
b702f5772d | ||
|
|
214e23fd93 | ||
|
|
943071ebb7 | ||
|
|
c575ed2036 | ||
|
|
06a178cc03 | ||
|
|
73f90885b4 | ||
|
|
9402109663 | ||
|
|
ace4e200b1 | ||
|
|
032388a8e3 | ||
|
|
276b6e521a | ||
|
|
35a4462a86 | ||
|
|
5564f378e5 | ||
|
|
66d5f3e4b5 | ||
|
|
0f367c23aa | ||
|
|
7dd1679588 | ||
|
|
6b186d5d8e | ||
|
|
d53be3aa14 | ||
|
|
3dbfde534e | ||
|
|
9a57842ece | ||
|
|
ec83b9a17d | ||
|
|
59a19b0091 | ||
|
|
471865e4a3 | ||
|
|
3868b4eca2 | ||
|
|
64f8765115 | ||
|
|
3804656218 | ||
|
|
a5d824213c | ||
|
|
160a4e9f8d | ||
|
|
74a0574462 | ||
|
|
1cf23b3542 | ||
|
|
2ef1242cd8 | ||
|
|
54c3bee205 | ||
|
|
a63c0a3625 | ||
|
|
3103a0bffc | ||
|
|
5d71ffbfaa | ||
|
|
c0c137d1b0 | ||
|
|
a05cfd52cb | ||
|
|
ad0967f7d0 | ||
|
|
8625db2885 | ||
|
|
a2a969a0a0 | ||
|
|
8a6386ca73 | ||
|
|
255c018415 | ||
|
|
df27ed6907 | ||
|
|
989491b903 | ||
|
|
664dd62d8b | ||
|
|
3d6b145db5 | ||
|
|
37a63242a6 | ||
|
|
dfb8c64f5a | ||
|
|
27bbf7a513 | ||
|
|
ca890e4b32 | ||
|
|
4834d01223 | ||
|
|
84b3bb1aed | ||
|
|
a9679da719 | ||
|
|
f10b3ffe3c | ||
|
|
ea982d6412 | ||
|
|
1035a3be31 | ||
|
|
c16db14cd9 | ||
|
|
1698f9d5df | ||
|
|
08e43cc89c | ||
|
|
fb1baaa275 | ||
|
|
eda838d6f8 | ||
|
|
2fff3083ae | ||
|
|
7e2d58b3e8 | ||
|
|
31ec385282 | ||
|
|
14ef7c0a72 | ||
|
|
c9331afeac | ||
|
|
09cb92e235 | ||
|
|
6bd6e6563b | ||
|
|
3c08c20426 | ||
|
|
3800e3e465 | ||
|
|
e2bd73dbf3 | ||
|
|
a20c3018ae | ||
|
|
7f17b33859 | ||
|
|
029937a1c5 | ||
|
|
cfcf02708c | ||
|
|
c1724ef74c | ||
|
|
3c53f97c36 | ||
|
|
aa895d10ac | ||
|
|
6c603618ce | ||
|
|
78aa0d4c61 | ||
|
|
da3fc72e5c | ||
|
|
a800766cb8 | ||
|
|
67a1529dc7 | ||
|
|
77354f47bf | ||
|
|
49ea07fd13 | ||
|
|
bb69c67b52 | ||
|
|
6b29c28e2e | ||
|
|
ba0fe43f31 | ||
|
|
e54597f108 | ||
|
|
9809ad9d7b | ||
|
|
1d4ad19acd | ||
|
|
2c24e754be | ||
|
|
4a3326a560 | ||
|
|
24862e2d4a | ||
|
|
51bb26ae34 | ||
|
|
f474651f36 | ||
|
|
11193b1b6d | ||
|
|
3066b3e500 | ||
|
|
217f112bc4 | ||
|
|
e1bb0cf5ec | ||
|
|
3930c25a75 | ||
|
|
b27e79cb52 | ||
|
|
daf6486b86 | ||
|
|
1af41f8ea5 | ||
|
|
70e4ac21df | ||
|
|
95d9d85ca2 | ||
|
|
9cc7a49d12 | ||
|
|
ae73595335 | ||
|
|
ac620082ec | ||
|
|
1e4134c7d1 | ||
|
|
a7bcdd0003 | ||
|
|
121af0501a | ||
|
|
82c4e9cf41 | ||
|
|
142e11a59a | ||
|
|
08205ed32d | ||
|
|
9246b42057 | ||
|
|
67e4d6e3a2 | ||
|
|
cf4613d043 | ||
|
|
2f703796e9 | ||
|
|
731dc3f3dc | ||
|
|
97472310f2 | ||
|
|
ece1d071c0 | ||
|
|
20f532c872 | ||
|
|
bdcccfd937 | ||
|
|
146b8f0103 | ||
|
|
c8a37245fa | ||
|
|
af68c70239 | ||
|
|
5bd544dcd7 | ||
|
|
642b85069d | ||
|
|
6fef4ea82c | ||
|
|
8670afb96f | ||
|
|
33f8d60900 | ||
|
|
4e2156ad79 | ||
|
|
a07245224e | ||
|
|
f96a4cdb59 | ||
|
|
efea22aa6e | ||
|
|
ae9a7dc580 | ||
|
|
7135ace1aa | ||
|
|
625275124a | ||
|
|
c96ebccae4 | ||
|
|
20cb460a7e | ||
|
|
3a62d19979 | ||
|
|
51ad741744 | ||
|
|
673f005fe6 | ||
|
|
8d9f0361d0 | ||
|
|
56e3ec1219 | ||
|
|
a0acafcefc | ||
|
|
918f5954b7 | ||
|
|
148dcdaee7 | ||
|
|
a5f0777892 | ||
|
|
010426d488 | ||
|
|
6fc9c7be90 | ||
|
|
094fde9801 | ||
|
|
e4a76e96af | ||
|
|
68f582bcb9 | ||
|
|
7e4d8f45f6 | ||
|
|
639aed7120 | ||
|
|
994bb4d1d2 | ||
|
|
80b11a55fa | ||
|
|
3cfb554623 | ||
|
|
e84f9c5946 | ||
|
|
17cc31d6e7 | ||
|
|
0c6e10a6dd | ||
|
|
297c57631f | ||
|
|
bd4e252bb6 | ||
|
|
0ce34c70bc | ||
|
|
4df75cfba1 | ||
|
|
2aa435bcfb | ||
|
|
0a4a9e6947 | ||
|
|
d5860d0b55 | ||
|
|
6a487ce514 | ||
|
|
4f3b85e66b | ||
|
|
370cd97e05 | ||
|
|
55d4fda01b | ||
|
|
3dd0471e22 | ||
|
|
5a4efa81a7 | ||
|
|
ea59c12b27 | ||
|
|
9600f91dda | ||
|
|
a9f93b014a | ||
|
|
bf62e389f7 | ||
|
|
26af6b5636 | ||
|
|
0fabfea56d | ||
|
|
7a9417a62f | ||
|
|
eb45005baa | ||
|
|
a4f923eb3a | ||
|
|
e1c3561997 | ||
|
|
bf8b902100 | ||
|
|
8a5883501a | ||
|
|
fd8f3ce019 | ||
|
|
6f72096953 | ||
|
|
5a52e3d6dd | ||
|
|
23672cd18d | ||
|
|
68c0941666 | ||
|
|
96e399a617 | ||
|
|
22343e507d | ||
|
|
8a143d139c | ||
|
|
15ad46fe1c | ||
|
|
2473f0d034 | ||
|
|
f211eefc85 | ||
|
|
9da88b7652 | ||
|
|
729c8006d2 | ||
|
|
0d5b790443 | ||
|
|
aa16035137 | ||
|
|
41686883ee | ||
|
|
2e0790c893 | ||
|
|
4e937a6024 | ||
|
|
4af58118c9 | ||
|
|
aa2ad33c1d | ||
|
|
c7005bc07f | ||
|
|
3f932ebec9 | ||
|
|
296f265391 | ||
|
|
ca8519cb10 | ||
|
|
734d76a3b5 | ||
|
|
f5a39ed514 | ||
|
|
96f4e5eac7 | ||
|
|
48be772703 | ||
|
|
a9edb49723 | ||
|
|
95471a2fba | ||
|
|
36c277961f | ||
|
|
734fff5618 | ||
|
|
16136b2f6f | ||
|
|
c8dfe10a7c | ||
|
|
c8481fdbef | ||
|
|
8e0c02a151 | ||
|
|
271906097d | ||
|
|
91caa2a097 | ||
|
|
6105451e08 | ||
|
|
450e090252 | ||
|
|
86635f85b4 | ||
|
|
a7214a2171 | ||
|
|
61cd473e6c | ||
|
|
d67690d995 | ||
|
|
e20f4fe31a | ||
|
|
2deb40bb6d | ||
|
|
bfee745cca | ||
|
|
68d390e633 | ||
|
|
a884f1b3d4 | ||
|
|
d190ae0cf3 | ||
|
|
bb08a63296 | ||
|
|
3debc1b0df | ||
|
|
e560028097 | ||
|
|
5afcbdbc8b | ||
|
|
aaaf3ffe34 | ||
|
|
b043e666ae | ||
|
|
cda8f7b27d | ||
|
|
d524fcc157 | ||
|
|
e05e34f217 | ||
|
|
73bbb516d2 | ||
|
|
c2aaa65228 | ||
|
|
91722af8b0 | ||
|
|
71d33f6047 | ||
|
|
903faadff3 | ||
|
|
55020d45f8 | ||
|
|
2d37c707e2 | ||
|
|
b00f8c80ec | ||
|
|
53afb865c5 | ||
|
|
6f3e317484 | ||
|
|
e77d2847ea | ||
|
|
46abae9acc | ||
|
|
571320b9ba | ||
|
|
07e9fcb6cc | ||
|
|
7a8719743d | ||
|
|
746f2d0949 | ||
|
|
3986858adb | ||
|
|
589cb8d91f | ||
|
|
705c75e51d | ||
|
|
6acb593411 | ||
|
|
8060451713 | ||
|
|
6130f14d5a | ||
|
|
0fbc461877 | ||
|
|
89e461e4f6 | ||
|
|
ba4ad1aff9 | ||
|
|
be1904749b | ||
|
|
166fa0eb87 | ||
|
|
9a06e7a3ca | ||
|
|
cb12af2d95 | ||
|
|
46032b8ebb | ||
|
|
4a6bd60466 | ||
|
|
f85c8ea5ec | ||
|
|
06ef09035d | ||
|
|
13301e4606 | ||
|
|
b59651a0fb | ||
|
|
86b8d7f804 | ||
|
|
e4dded3faa | ||
|
|
75cf3ed0c1 | ||
|
|
2fa68be36b | ||
|
|
d09b252a4a | ||
|
|
a5165b04cd | ||
|
|
1bd17eded6 | ||
|
|
18d289d3b7 | ||
|
|
e43e6d18b9 | ||
|
|
9dbc2712e7 | ||
|
|
ec5a3c5948 | ||
|
|
89c0e721b8 | ||
|
|
c807d20590 | ||
|
|
686af16cf5 | ||
|
|
219dd7834f | ||
|
|
1b83fda349 | ||
|
|
490acddc65 | ||
|
|
e69ed06b4f |
@@ -1,2 +1,5 @@
|
|||||||
# Formatting
|
# Formatting
|
||||||
5f771b785130154ed47952635b7acef371ffe0ec
|
5f771b785130154ed47952635b7acef371ffe0ec
|
||||||
|
|
||||||
|
# Normalize files
|
||||||
|
55d4fda01b2f39f5b7d7b4fda5214bd7ff0fd5dd
|
||||||
|
|||||||
18
.github/pull_request_template.md
vendored
18
.github/pull_request_template.md
vendored
@@ -1,4 +1,18 @@
|
|||||||
# License Agreement for Contributions
|
# Description
|
||||||
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.
|
|
||||||
|
Please provide a summary of the changes, including relevant motivation and context.
|
||||||
|
|
||||||
|
Closes #(issue_number)
|
||||||
|
|
||||||
|
## Checklist:
|
||||||
|
|
||||||
|
- [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
|
||||||
|
- [ ] I have performed a self-review of my own code
|
||||||
|
- [ ] I have commented my code, particularly in hard-to-understand areas
|
||||||
|
- [ ] My changes generate no new warnings
|
||||||
|
|
||||||
|
## Contributor License Agreement
|
||||||
|
|
||||||
|
By submitting this pull request, I acknowledge and agree that my contributions will be included in Stirling-PDF and that they can be relicensed in the future under the MPL 2.0 (Mozilla Public License Version 2.0) license.
|
||||||
|
|
||||||
(This does not change the general open-source nature of Stirling-PDF, simply moving from one license to another license)
|
(This does not change the general open-source nature of Stirling-PDF, simply moving from one license to another license)
|
||||||
|
|||||||
51
.github/scripts/check_duplicates.py
vendored
Normal file
51
.github/scripts/check_duplicates.py
vendored
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def find_duplicate_keys(file_path):
|
||||||
|
"""
|
||||||
|
Finds duplicate keys in a properties file and returns their occurrences.
|
||||||
|
|
||||||
|
This function reads a properties file, identifies any keys that occur more than
|
||||||
|
once, and returns a dictionary with these keys and the line numbers of their occurrences.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
file_path (str): The path to the properties file to be checked.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: A dictionary where each key is a duplicated key in the file, and the value is a list
|
||||||
|
of line numbers where the key occurs.
|
||||||
|
"""
|
||||||
|
with open(file_path, "r", encoding="utf-8") as file:
|
||||||
|
lines = file.readlines()
|
||||||
|
|
||||||
|
keys = {}
|
||||||
|
duplicates = {}
|
||||||
|
|
||||||
|
for line_number, line in enumerate(lines, start=1):
|
||||||
|
line = line.strip()
|
||||||
|
if line and not line.startswith("#") and "=" in line:
|
||||||
|
key = line.split("=", 1)[0].strip()
|
||||||
|
if key in keys:
|
||||||
|
# If the key already exists, add the current line number
|
||||||
|
duplicates.setdefault(key, []).append(line_number)
|
||||||
|
# Also add the first instance of the key if not already done
|
||||||
|
if keys[key] not in duplicates[key]:
|
||||||
|
duplicates[key].insert(0, keys[key])
|
||||||
|
else:
|
||||||
|
# Store the line number of the first instance of the key
|
||||||
|
keys[key] = line_number
|
||||||
|
|
||||||
|
return duplicates
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
failed = False
|
||||||
|
for ar in sys.argv[1:]:
|
||||||
|
duplicates = find_duplicate_keys(ar)
|
||||||
|
if duplicates:
|
||||||
|
for key, lines in duplicates.items():
|
||||||
|
lines_str = ", ".join(map(str, lines))
|
||||||
|
print(f"{key} duplicated in {ar} on lines {lines_str}")
|
||||||
|
failed = True
|
||||||
|
if failed:
|
||||||
|
sys.exit(1)
|
||||||
84
.github/scripts/check_tabulator.py
vendored
Normal file
84
.github/scripts/check_tabulator.py
vendored
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
"""check_tabulator.py"""
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def check_tabs(file_path):
|
||||||
|
"""
|
||||||
|
Checks for tabs in the specified file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_path (str): The path to the file to be checked.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if tabs are found, False otherwise.
|
||||||
|
"""
|
||||||
|
with open(file_path, "r", encoding="utf-8") as file:
|
||||||
|
content = file.read()
|
||||||
|
|
||||||
|
if "\t" in content:
|
||||||
|
print(f"Tab found in {file_path}")
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def replace_tabs_with_spaces(file_path, replace_with=" "):
|
||||||
|
"""
|
||||||
|
Replaces tabs with a specified number of spaces in the file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_path (str): The path to the file where tabs will be replaced.
|
||||||
|
replace_with (str): The character(s) to replace tabs with. Defaults to two spaces.
|
||||||
|
"""
|
||||||
|
with open(file_path, "r", encoding="utf-8") as file:
|
||||||
|
content = file.read()
|
||||||
|
|
||||||
|
updated_content = content.replace("\t", replace_with)
|
||||||
|
|
||||||
|
with open(file_path, "w", encoding="utf-8") as file:
|
||||||
|
file.write(updated_content)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""
|
||||||
|
Main function to replace tabs with spaces in the provided files.
|
||||||
|
The replacement character and files to check are taken from command line arguments.
|
||||||
|
"""
|
||||||
|
# Create ArgumentParser instance
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Replace tabs in files with specified characters."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Define optional argument `--replace_with`
|
||||||
|
parser.add_argument(
|
||||||
|
"--replace_with",
|
||||||
|
default=" ",
|
||||||
|
help="Character(s) to replace tabs with. Default is two spaces.",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Define argument for file paths
|
||||||
|
parser.add_argument("files", metavar="FILE", nargs="+", help="Files to process.")
|
||||||
|
|
||||||
|
# Parse arguments
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Extract replacement characters and files from the parsed arguments
|
||||||
|
replace_with = args.replace_with
|
||||||
|
files_checked = args.files
|
||||||
|
|
||||||
|
error = False
|
||||||
|
|
||||||
|
for file_path in files_checked:
|
||||||
|
if check_tabs(file_path):
|
||||||
|
replace_tabs_with_spaces(file_path, replace_with)
|
||||||
|
error = True
|
||||||
|
|
||||||
|
if error:
|
||||||
|
print("Error: Originally found tabs in HTML files, now replaced.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
67
.github/scripts/gradle_to_chart.py
vendored
Normal file
67
.github/scripts/gradle_to_chart.py
vendored
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import re
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
# Paths to the files
|
||||||
|
chart_yaml_path = "chart/stirling-pdf/Chart.yaml"
|
||||||
|
gradle_path = "build.gradle"
|
||||||
|
|
||||||
|
|
||||||
|
def get_chart_version(path):
|
||||||
|
"""
|
||||||
|
Reads the appVersion from Chart.yaml.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path (str): The file path to the Chart.yaml.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: The appVersion if found, otherwise an empty string.
|
||||||
|
"""
|
||||||
|
with open(path, encoding="utf-8") as file:
|
||||||
|
chart_yaml = yaml.safe_load(file)
|
||||||
|
return chart_yaml.get("appVersion", "")
|
||||||
|
|
||||||
|
|
||||||
|
def get_gradle_version(path):
|
||||||
|
"""
|
||||||
|
Extracts the version from build.gradle.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path (str): The file path to the build.gradle.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: The version if found, otherwise an empty string.
|
||||||
|
"""
|
||||||
|
with open(path, encoding="utf-8") as file:
|
||||||
|
for line in file:
|
||||||
|
if "version =" in line:
|
||||||
|
# Extracts the value after 'version ='
|
||||||
|
return re.search(r'version\s*=\s*[\'"](.+?)[\'"]', line).group(1)
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def update_chart_version(path, new_version):
|
||||||
|
"""
|
||||||
|
Updates the appVersion in Chart.yaml with a new version.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path (str): The file path to the Chart.yaml.
|
||||||
|
new_version (str): The new version to update to.
|
||||||
|
"""
|
||||||
|
with open(path, encoding="utf-8") as file:
|
||||||
|
chart_yaml = yaml.safe_load(file)
|
||||||
|
chart_yaml["appVersion"] = new_version
|
||||||
|
with open(path, "w", encoding="utf-8") as file:
|
||||||
|
yaml.safe_dump(chart_yaml, file)
|
||||||
|
|
||||||
|
|
||||||
|
# Main logic
|
||||||
|
chart_version = get_chart_version(chart_yaml_path)
|
||||||
|
gradle_version = get_gradle_version(gradle_path)
|
||||||
|
|
||||||
|
if chart_version != gradle_version:
|
||||||
|
print(
|
||||||
|
f"Versions do not match. Updating Chart.yaml from {chart_version} to {gradle_version}."
|
||||||
|
)
|
||||||
|
update_chart_version(chart_yaml_path, gradle_version)
|
||||||
|
else:
|
||||||
|
print("Versions match. No update required.")
|
||||||
34
.github/workflows/build.yml
vendored
34
.github/workflows/build.yml
vendored
@@ -2,9 +2,15 @@ name: "Build repo"
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ "main" ]
|
branches: ["main"]
|
||||||
|
paths-ignore:
|
||||||
|
- ".github/**"
|
||||||
|
- "**/*.md"
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ "main" ]
|
branches: ["main"]
|
||||||
|
paths-ignore:
|
||||||
|
- ".github/**"
|
||||||
|
- "**/*.md"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -19,16 +25,18 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up JDK 17
|
- name: Set up JDK 17
|
||||||
uses: actions/setup-java@v3
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
java-version: '17'
|
java-version: "17"
|
||||||
distribution: 'temurin'
|
distribution: "temurin"
|
||||||
|
|
||||||
- uses: gradle/gradle-build-action@v2.4.2
|
- uses: gradle/actions/setup-gradle@v3
|
||||||
with:
|
with:
|
||||||
gradle-version: 7.6
|
gradle-version: 7.6
|
||||||
arguments: build --no-build-cache
|
|
||||||
|
- name: Build with Gradle
|
||||||
|
run: ./gradlew build --no-build-cache
|
||||||
|
|||||||
46
.github/workflows/licenses-update.yml
vendored
46
.github/workflows/licenses-update.yml
vendored
@@ -5,7 +5,7 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
paths:
|
paths:
|
||||||
- 'build.gradle'
|
- "build.gradle"
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
@@ -17,13 +17,15 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code
|
- name: Check out code
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up JDK 17
|
- name: Set up JDK 17
|
||||||
uses: actions/setup-java@v3
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
java-version: '17'
|
java-version: "17"
|
||||||
distribution: 'adopt'
|
distribution: "adopt"
|
||||||
|
|
||||||
|
- uses: gradle/actions/setup-gradle@v3
|
||||||
|
|
||||||
- name: Run Gradle Command
|
- name: Run Gradle Command
|
||||||
run: ./gradlew clean generateLicenseReport
|
run: ./gradlew clean generateLicenseReport
|
||||||
@@ -32,17 +34,29 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
mv build/reports/dependency-license/index.json src/main/resources/static/3rdPartyLicenses.json
|
mv build/reports/dependency-license/index.json src/main/resources/static/3rdPartyLicenses.json
|
||||||
|
|
||||||
- name: Check for Changes
|
- name: Set up git config
|
||||||
id: git-check
|
run: |
|
||||||
|
git config --global user.email "GitHub Action <action@github.com>"
|
||||||
|
git config --global user.name "GitHub Action <action@github.com>"
|
||||||
|
|
||||||
|
- name: Run git add
|
||||||
run: |
|
run: |
|
||||||
git add src/main/resources/static/3rdPartyLicenses.json
|
git add src/main/resources/static/3rdPartyLicenses.json
|
||||||
git diff --staged --exit-code || echo "changes=true" >> $GITHUB_ENV
|
git diff --staged --quiet || echo "CHANGES_DETECTED=true" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Commit and Push Changes
|
|
||||||
if: env.changes == 'true'
|
|
||||||
run: |
|
|
||||||
git config --global user.name 'Stirling-PDF-Bot'
|
|
||||||
git config --global user.email 'Stirling-PDF-Bot@stirlingtools.com'
|
|
||||||
git commit -m "Update 3rd Party Licenses"
|
|
||||||
git push
|
|
||||||
|
|
||||||
|
- name: Create Pull Request
|
||||||
|
if: env.CHANGES_DETECTED == 'true'
|
||||||
|
uses: peter-evans/create-pull-request@v6
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
commit-message: "Update 3rd Party Licenses"
|
||||||
|
committer: GitHub Action <action@github.com>
|
||||||
|
author: GitHub Action <action@github.com>
|
||||||
|
signoff: true
|
||||||
|
branch: update-3rd-party-licenses
|
||||||
|
title: "Update 3rd Party Licenses"
|
||||||
|
body: |
|
||||||
|
Auto-generated by [create-pull-request][1]
|
||||||
|
[1]: https://github.com/peter-evans/create-pull-request
|
||||||
|
draft: false
|
||||||
|
delete-branch: true
|
||||||
|
|||||||
3
.github/workflows/pull_request_template.md
vendored
3
.github/workflows/pull_request_template.md
vendored
@@ -1,3 +0,0 @@
|
|||||||
# 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)
|
|
||||||
209
.github/workflows/push-docker.yml
vendored
209
.github/workflows/push-docker.yml
vendored
@@ -3,9 +3,10 @@ name: Push Docker Image with VersionNumber
|
|||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
- main
|
- main
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
packages: write
|
packages: write
|
||||||
@@ -13,139 +14,99 @@ jobs:
|
|||||||
push:
|
push:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: actions/checkout@v3.5.2
|
- name: Set up JDK 17
|
||||||
|
uses: actions/setup-java@v4
|
||||||
- name: Set up JDK 17
|
with:
|
||||||
uses: actions/setup-java@v3.11.0
|
java-version: "17"
|
||||||
with:
|
distribution: "temurin"
|
||||||
java-version: '17'
|
|
||||||
distribution: 'temurin'
|
|
||||||
|
|
||||||
|
|
||||||
- uses: gradle/gradle-build-action@v2.4.2
|
- uses: gradle/actions/setup-gradle@v3
|
||||||
env:
|
with:
|
||||||
DOCKER_ENABLE_SECURITY: false
|
gradle-version: 7.6
|
||||||
with:
|
|
||||||
gradle-version: 7.6
|
|
||||||
arguments: clean build
|
|
||||||
|
|
||||||
- name: Make Gradle wrapper executable
|
- name: Run Gradle Command
|
||||||
run: chmod +x gradlew
|
run: ./gradlew clean build
|
||||||
|
env:
|
||||||
- name: Get version number
|
DOCKER_ENABLE_SECURITY: false
|
||||||
id: versionNumber
|
|
||||||
run: echo "::set-output name=versionNumber::$(./gradlew printVersion --quiet | tail -1)"
|
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
|
||||||
uses: docker/login-action@v2.1.0
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKER_HUB_API }}
|
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Set up Docker Buildx
|
||||||
uses: docker/login-action@v2.1.0
|
id: buildx
|
||||||
with:
|
uses: docker/setup-buildx-action@v3
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ github.actor }}
|
|
||||||
password: ${{ github.token }}
|
|
||||||
|
|
||||||
- name: Convert repository owner to lowercase
|
- name: Get version number
|
||||||
id: repoowner
|
id: versionNumber
|
||||||
run: echo "::set-output name=lowercase::$(echo ${{ github.repository_owner }} | awk '{print tolower($0)}')"
|
run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Generate tags
|
|
||||||
id: meta
|
|
||||||
uses: docker/metadata-action@v4.4.0
|
|
||||||
with:
|
|
||||||
images: |
|
|
||||||
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
|
|
||||||
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/s-pdf
|
|
||||||
tags: |
|
|
||||||
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=alpha,enable=${{ github.ref == 'refs/heads/main' }}
|
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Login to Docker Hub
|
||||||
uses: docker/setup-qemu-action@v2.1.0
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_HUB_API }}
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/setup-buildx-action@v2.5.0
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ github.token }}
|
||||||
|
|
||||||
- name: Build and push main Dockerfile
|
- name: Set up QEMU
|
||||||
uses: docker/build-push-action@v4.0.0
|
uses: docker/setup-qemu-action@v3
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
dockerfile: ./Dockerfile
|
|
||||||
push: true
|
|
||||||
cache-from: type=gha
|
|
||||||
cache-to: type=gha,mode=max
|
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
|
||||||
build-args:
|
|
||||||
VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
|
|
||||||
platforms: linux/amd64,linux/arm64/v8
|
|
||||||
|
|
||||||
|
- name: Convert repository owner to lowercase
|
||||||
|
id: repoowner
|
||||||
|
run: echo "lowercase=$(echo ${{ github.repository_owner }} | awk '{print tolower($0)}')" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Generate tags
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: |
|
||||||
|
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
|
||||||
|
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/s-pdf
|
||||||
|
tags: |
|
||||||
|
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=alpha,enable=${{ github.ref == 'refs/heads/main' }}
|
||||||
|
|
||||||
- name: Generate tags ultra-lite
|
- name: Build and push main Dockerfile
|
||||||
id: meta2
|
uses: docker/build-push-action@v5
|
||||||
uses: docker/metadata-action@v4.4.0
|
with:
|
||||||
if: github.ref != 'refs/heads/main'
|
builder: ${{ steps.buildx.outputs.name }}
|
||||||
with:
|
context: .
|
||||||
images: |
|
file: ./Dockerfile
|
||||||
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
|
push: true
|
||||||
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/s-pdf
|
cache-from: type=gha
|
||||||
tags: |
|
cache-to: type=gha,mode=max
|
||||||
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}-ultra-lite,enable=${{ github.ref == 'refs/heads/master' }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
type=raw,value=latest-ultra-lite,enable=${{ github.ref == 'refs/heads/master' }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
build-args: VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
|
||||||
|
platforms: linux/amd64,linux/arm64/v8
|
||||||
|
|
||||||
- name: Build and push Dockerfile-ultra-lite
|
- name: Generate tags ultra-lite
|
||||||
uses: docker/build-push-action@v4.0.0
|
id: meta2
|
||||||
if: github.ref != 'refs/heads/main'
|
uses: docker/metadata-action@v5
|
||||||
with:
|
if: github.ref != 'refs/heads/main'
|
||||||
context: .
|
with:
|
||||||
file: ./Dockerfile-ultra-lite
|
images: |
|
||||||
push: true
|
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
|
||||||
cache-from: type=gha
|
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/s-pdf
|
||||||
cache-to: type=gha,mode=max
|
tags: |
|
||||||
tags: ${{ steps.meta2.outputs.tags }}
|
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}-ultra-lite,enable=${{ github.ref == 'refs/heads/master' }}
|
||||||
labels: ${{ steps.meta2.outputs.labels }}
|
type=raw,value=latest-ultra-lite,enable=${{ github.ref == 'refs/heads/master' }}
|
||||||
build-args:
|
|
||||||
VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
|
|
||||||
platforms: linux/amd64,linux/arm64/v8
|
|
||||||
|
|
||||||
|
- name: Build and push Dockerfile-ultra-lite
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
- name: Generate tags lite
|
if: github.ref != 'refs/heads/main'
|
||||||
id: meta3
|
with:
|
||||||
uses: docker/metadata-action@v4.4.0
|
context: .
|
||||||
if: github.ref != 'refs/heads/main'
|
file: ./Dockerfile-ultra-lite
|
||||||
with:
|
push: true
|
||||||
images: |
|
cache-from: type=gha
|
||||||
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
|
cache-to: type=gha,mode=max
|
||||||
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/s-pdf
|
tags: ${{ steps.meta2.outputs.tags }}
|
||||||
tags: |
|
labels: ${{ steps.meta2.outputs.labels }}
|
||||||
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}-lite,enable=${{ github.ref == 'refs/heads/master' }}
|
build-args: VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
|
||||||
type=raw,value=latest-lite,enable=${{ github.ref == 'refs/heads/master' }}
|
platforms: linux/amd64,linux/arm64/v8
|
||||||
|
|
||||||
|
|
||||||
- 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
|
|
||||||
|
|||||||
96
.github/workflows/releaseArtifacts.yml
vendored
96
.github/workflows/releaseArtifacts.yml
vendored
@@ -1,7 +1,8 @@
|
|||||||
name: Release Artifacts
|
name: Release Artifacts
|
||||||
|
|
||||||
on:
|
on:
|
||||||
release:
|
workflow_dispatch:
|
||||||
|
release:
|
||||||
types: [created]
|
types: [created]
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
@@ -14,44 +15,61 @@ jobs:
|
|||||||
enable_security: [true, false]
|
enable_security: [true, false]
|
||||||
include:
|
include:
|
||||||
- enable_security: true
|
- enable_security: true
|
||||||
file_suffix: '-with-login'
|
file_suffix: "-with-login"
|
||||||
- enable_security: false
|
- enable_security: false
|
||||||
file_suffix: ''
|
file_suffix: ""
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3.5.2
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- 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 }})
|
- name: Set up JDK 17
|
||||||
run: ./gradlew clean createExe
|
uses: actions/setup-java@v4
|
||||||
env:
|
with:
|
||||||
DOCKER_ENABLE_SECURITY: ${{ matrix.enable_security }}
|
java-version: "17"
|
||||||
|
distribution: "temurin"
|
||||||
|
|
||||||
- name: Upload binaries to release
|
- uses: gradle/actions/setup-gradle@v3
|
||||||
uses: svenstaro/upload-release-action@v2
|
with:
|
||||||
with:
|
gradle-version: 7.6
|
||||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
file: ./build/launch4j/Stirling-PDF.exe
|
- name: Generate jar (With Security=${{ matrix.enable_security }})
|
||||||
asset_name: Stirling-PDF${{ matrix.file_suffix }}.exe
|
run: ./gradlew clean createExe
|
||||||
tag: ${{ github.ref }}
|
env:
|
||||||
overwrite: true
|
DOCKER_ENABLE_SECURITY: ${{ matrix.enable_security }}
|
||||||
|
|
||||||
- name: Get version number
|
- name: Get version number
|
||||||
id: versionNumber
|
id: versionNumber
|
||||||
run: echo "::set-output name=versionNumber::$(./gradlew printVersion --quiet | tail -1)"
|
run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Upload jar binaries to release
|
- name: Rename binarie
|
||||||
uses: svenstaro/upload-release-action@v2
|
if: matrix.file_suffix != ''
|
||||||
with:
|
run: cp ./build/launch4j/Stirling-PDF.exe ./build/launch4j/Stirling-PDF${{ matrix.file_suffix }}.exe
|
||||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
file: ./build/libs/Stirling-PDF-${{ steps.versionNumber.outputs.versionNumber }}.jar
|
- name: Upload Assets binarie
|
||||||
asset_name: Stirling-PDF${{ matrix.file_suffix }}.jar
|
uses: actions/upload-artifact@v4
|
||||||
tag: ${{ github.ref }}
|
with:
|
||||||
overwrite: true
|
path: ./build/launch4j/Stirling-PDF${{ matrix.file_suffix }}.exe
|
||||||
|
name: Stirling-PDF${{ matrix.file_suffix }}.exe
|
||||||
|
overwrite: true
|
||||||
|
retention-days: 1
|
||||||
|
if-no-files-found: error
|
||||||
|
- name: Upload binaries to release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
files: ./build/launch4j/Stirling-PDF${{ matrix.file_suffix }}.exe
|
||||||
|
|
||||||
|
- name: Rename jar binaries
|
||||||
|
run: cp ./build/libs/Stirling-PDF-${{ steps.versionNumber.outputs.versionNumber }}.jar ./build/libs/Stirling-PDF${{ matrix.file_suffix }}.jar
|
||||||
|
|
||||||
|
- name: Upload Assets jar binaries
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
path: ./build/libs/Stirling-PDF${{ matrix.file_suffix }}.jar
|
||||||
|
name: Stirling-PDF${{ matrix.file_suffix }}.jar
|
||||||
|
overwrite: true
|
||||||
|
retention-days: 1
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
- name: Upload jar binaries to release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
files: ./build/libs/Stirling-PDF${{ matrix.file_suffix }}.jar
|
||||||
|
|||||||
48
.github/workflows/swagger.yml
vendored
48
.github/workflows/swagger.yml
vendored
@@ -3,35 +3,37 @@ name: Update Swagger
|
|||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
push:
|
push:
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: actions/checkout@v3.5.2
|
- name: Set up JDK 17
|
||||||
|
uses: actions/setup-java@v4
|
||||||
- name: Set up JDK 17
|
with:
|
||||||
uses: actions/setup-java@v3.11.0
|
java-version: "17"
|
||||||
with:
|
distribution: "temurin"
|
||||||
java-version: '17'
|
|
||||||
distribution: 'temurin'
|
|
||||||
|
|
||||||
- name: Grant execute permission for gradlew
|
|
||||||
run: chmod +x gradlew
|
|
||||||
|
|
||||||
- name: Generate Swagger documentation
|
- uses: gradle/actions/setup-gradle@v3
|
||||||
run: ./gradlew generateOpenApiDocs
|
|
||||||
|
|
||||||
- name: Upload Swagger Documentation to SwaggerHub
|
- name: Generate Swagger documentation
|
||||||
run: ./gradlew swaggerhubUpload
|
run: ./gradlew generateOpenApiDocs
|
||||||
env:
|
|
||||||
SWAGGERHUB_API_KEY: ${{ secrets.SWAGGERHUB_API_KEY }}
|
|
||||||
|
|
||||||
- name: Set API version as published and default on SwaggerHub
|
- name: Upload Swagger Documentation to SwaggerHub
|
||||||
run: |
|
run: ./gradlew swaggerhubUpload
|
||||||
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:
|
||||||
env:
|
SWAGGERHUB_API_KEY: ${{ secrets.SWAGGERHUB_API_KEY }}
|
||||||
SWAGGERHUB_API_KEY: ${{ secrets.SWAGGERHUB_API_KEY }}
|
|
||||||
|
- name: Get version number
|
||||||
|
id: versionNumber
|
||||||
|
run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- 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 }}
|
||||||
|
|||||||
90
.github/workflows/sync_files.yml
vendored
Normal file
90
.github/workflows/sync_files.yml
vendored
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
name: Sync Files
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- "build.gradle"
|
||||||
|
- "src/main/resources/messages_*.properties"
|
||||||
|
- "scripts/translation_status.toml"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
sync-versions:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4.1.1
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5.1.0
|
||||||
|
with:
|
||||||
|
python-version: "3.x"
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pip install pyyaml
|
||||||
|
- name: Sync versions
|
||||||
|
run: python .github/scripts/gradle_to_chart.py
|
||||||
|
- name: Set up git config
|
||||||
|
run: |
|
||||||
|
git config --global user.email "GitHub Action <action@github.com>"
|
||||||
|
git config --global user.name "GitHub Action <action@github.com>"
|
||||||
|
- name: Run git add
|
||||||
|
run: |
|
||||||
|
git add .
|
||||||
|
git diff --staged --quiet || git commit -m ":floppy_disk: Sync Versions
|
||||||
|
> Made via sync_files.yml" || echo "no changes"
|
||||||
|
- name: Create Pull Request
|
||||||
|
uses: peter-evans/create-pull-request@v6.0.1
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
commit-message: Update files
|
||||||
|
committer: GitHub Action <action@github.com>
|
||||||
|
author: GitHub Action <action@github.com>
|
||||||
|
signoff: true
|
||||||
|
branch: sync_version
|
||||||
|
title: ":floppy_disk: Update Version"
|
||||||
|
body: |
|
||||||
|
Auto-generated by [create-pull-request][1]
|
||||||
|
|
||||||
|
[1]: https://github.com/peter-evans/create-pull-request
|
||||||
|
draft: false
|
||||||
|
delete-branch: true
|
||||||
|
sync-readme:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4.1.1
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5.1.0
|
||||||
|
with:
|
||||||
|
python-version: "3.x"
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pip install tomlkit
|
||||||
|
- name: Sync README
|
||||||
|
run: python scripts/counter_translation.py
|
||||||
|
- name: Set up git config
|
||||||
|
run: |
|
||||||
|
git config --global user.email "GitHub Action <action@github.com>"
|
||||||
|
git config --global user.name "GitHub Action <action@github.com>"
|
||||||
|
- name: Run git add
|
||||||
|
run: |
|
||||||
|
git add .
|
||||||
|
git diff --staged --quiet || git commit -m ":memo: Sync README
|
||||||
|
> Made via sync_files.yml" || echo "no changes"
|
||||||
|
- name: Create Pull Request
|
||||||
|
uses: peter-evans/create-pull-request@v6.0.1
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
commit-message: Update files
|
||||||
|
committer: GitHub Action <action@github.com>
|
||||||
|
author: GitHub Action <action@github.com>
|
||||||
|
signoff: true
|
||||||
|
branch: sync_readme
|
||||||
|
title: ":memo: Update README: Translation Progress Table"
|
||||||
|
body: |
|
||||||
|
Auto-generated by [create-pull-request][1]
|
||||||
|
|
||||||
|
[1]: https://github.com/peter-evans/create-pull-request
|
||||||
|
draft: false
|
||||||
|
delete-branch: true
|
||||||
64
.github/workflows/test.yml
vendored
64
.github/workflows/test.yml
vendored
@@ -3,54 +3,36 @@ name: Docker Compose Tests
|
|||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- 'src/**'
|
- "src/**"
|
||||||
- '**.gradle'
|
- "**.gradle"
|
||||||
- '!src/main/java/resources/messages*'
|
- "!src/main/java/resources/messages*"
|
||||||
- 'exampleYmlFiles/**'
|
- "exampleYmlFiles/**"
|
||||||
- 'Dockerfile'
|
- "Dockerfile"
|
||||||
- 'Dockerfile**'
|
- "Dockerfile**"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Repository
|
- name: Checkout Repository
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Java 17
|
- name: Set up Java 17
|
||||||
uses: actions/setup-java@v2
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
java-version: '17'
|
java-version: "17"
|
||||||
distribution: 'adopt'
|
distribution: "adopt"
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v1
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Run Docker Compose Tests
|
- name: Install Docker Compose
|
||||||
run: |
|
run: |
|
||||||
chmod +x ./gradlew
|
sudo curl -SL "https://github.com/docker/compose/releases/download/v2.26.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
||||||
|
# sudo chmod +x /usr/local/bin/docker-compose
|
||||||
- name: Get version number
|
|
||||||
id: versionNumber
|
|
||||||
run: echo "::set-output name=versionNumber::$(./gradlew printVersion --quiet | tail -1)"
|
|
||||||
|
|
||||||
|
- name: Run Docker Compose Tests
|
||||||
- name: Cache Docker layers
|
run: |
|
||||||
uses: actions/cache@v2
|
chmod +x ./test.sh
|
||||||
with:
|
./test.sh
|
||||||
path: /tmp/.buildx-cache
|
|
||||||
key: ${{ runner.os }}-buildx-${{ steps.versionNumber.outputs.versionNumber }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-buildx-
|
|
||||||
|
|
||||||
- name: Install Docker Compose
|
|
||||||
run: |
|
|
||||||
sudo curl -L "https://github.com/docker/compose/releases/download/v2.6.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
|
||||||
sudo chmod +x /usr/local/bin/docker-compose
|
|
||||||
|
|
||||||
|
|
||||||
- name: Run Docker Compose Tests
|
|
||||||
run: |
|
|
||||||
chmod +x ./test.sh
|
|
||||||
./test.sh
|
|
||||||
|
|||||||
252
.gitignore
vendored
252
.gitignore
vendored
@@ -1,127 +1,127 @@
|
|||||||
|
|
||||||
|
|
||||||
### Eclipse ###
|
### Eclipse ###
|
||||||
.metadata
|
.metadata
|
||||||
bin/
|
bin/
|
||||||
tmp/
|
tmp/
|
||||||
*.tmp
|
*.tmp
|
||||||
*.bak
|
*.bak
|
||||||
*.swp
|
*.swp
|
||||||
*~.nib
|
*~.nib
|
||||||
local.properties
|
local.properties
|
||||||
.settings/
|
.settings/
|
||||||
.loadpath
|
.loadpath
|
||||||
.recommenders
|
.recommenders
|
||||||
.classpath
|
.classpath
|
||||||
.project
|
.project
|
||||||
version.properties
|
version.properties
|
||||||
pipeline/watchedFolders/
|
pipeline/watchedFolders/
|
||||||
pipeline/finishedFolders/
|
pipeline/finishedFolders/
|
||||||
#### Stirling-PDF Files ###
|
#### Stirling-PDF Files ###
|
||||||
customFiles/
|
customFiles/
|
||||||
configs/
|
configs/
|
||||||
watchedFolders/
|
watchedFolders/
|
||||||
|
|
||||||
|
|
||||||
# Gradle
|
# Gradle
|
||||||
.gradle
|
.gradle
|
||||||
.lock
|
.lock
|
||||||
|
|
||||||
# External tool builders
|
# External tool builders
|
||||||
.externalToolBuilders/
|
.externalToolBuilders/
|
||||||
|
|
||||||
# Locally stored "Eclipse launch configurations"
|
# Locally stored "Eclipse launch configurations"
|
||||||
*.launch
|
*.launch
|
||||||
|
|
||||||
# PyDev specific (Python IDE for Eclipse)
|
# PyDev specific (Python IDE for Eclipse)
|
||||||
*.pydevproject
|
*.pydevproject
|
||||||
|
|
||||||
# CDT-specific (C/C++ Development Tooling)
|
# CDT-specific (C/C++ Development Tooling)
|
||||||
.cproject
|
.cproject
|
||||||
|
|
||||||
# CDT- autotools
|
# CDT- autotools
|
||||||
.autotools
|
.autotools
|
||||||
|
|
||||||
# Java annotation processor (APT)
|
# Java annotation processor (APT)
|
||||||
.factorypath
|
.factorypath
|
||||||
|
|
||||||
# PDT-specific (PHP Development Tools)
|
# PDT-specific (PHP Development Tools)
|
||||||
.buildpath
|
.buildpath
|
||||||
|
|
||||||
# sbteclipse plugin
|
# sbteclipse plugin
|
||||||
.target
|
.target
|
||||||
|
|
||||||
# Tern plugin
|
# Tern plugin
|
||||||
.tern-project
|
.tern-project
|
||||||
|
|
||||||
# TeXlipse plugin
|
# TeXlipse plugin
|
||||||
.texlipse
|
.texlipse
|
||||||
|
|
||||||
# STS (Spring Tool Suite)
|
# STS (Spring Tool Suite)
|
||||||
.springBeans
|
.springBeans
|
||||||
|
|
||||||
# Code Recommenders
|
# Code Recommenders
|
||||||
.recommenders/
|
.recommenders/
|
||||||
|
|
||||||
# Annotation Processing
|
# Annotation Processing
|
||||||
.apt_generated/
|
.apt_generated/
|
||||||
.apt_generated_test/
|
.apt_generated_test/
|
||||||
|
|
||||||
# Scala IDE specific (Scala & Java development for Eclipse)
|
# Scala IDE specific (Scala & Java development for Eclipse)
|
||||||
.cache-main
|
.cache-main
|
||||||
.scala_dependencies
|
.scala_dependencies
|
||||||
.worksheet
|
.worksheet
|
||||||
|
|
||||||
# Uncomment this line if you wish to ignore the project description file.
|
# Uncomment this line if you wish to ignore the project description file.
|
||||||
# Typically, this file would be tracked if it contains build/dependency configurations:
|
# Typically, this file would be tracked if it contains build/dependency configurations:
|
||||||
#.project
|
#.project
|
||||||
|
|
||||||
### Eclipse Patch ###
|
### Eclipse Patch ###
|
||||||
# Spring Boot Tooling
|
# Spring Boot Tooling
|
||||||
.sts4-cache/
|
.sts4-cache/
|
||||||
|
|
||||||
### Git ###
|
### Git ###
|
||||||
# Created by git for backups. To disable backups in Git:
|
# Created by git for backups. To disable backups in Git:
|
||||||
# $ git config --global mergetool.keepBackup false
|
# $ git config --global mergetool.keepBackup false
|
||||||
*.orig
|
*.orig
|
||||||
|
|
||||||
# Created by git when using merge tools for conflicts
|
# Created by git when using merge tools for conflicts
|
||||||
*.BACKUP.*
|
*.BACKUP.*
|
||||||
*.BASE.*
|
*.BASE.*
|
||||||
*.LOCAL.*
|
*.LOCAL.*
|
||||||
*.REMOTE.*
|
*.REMOTE.*
|
||||||
*_BACKUP_*.txt
|
*_BACKUP_*.txt
|
||||||
*_BASE_*.txt
|
*_BASE_*.txt
|
||||||
*_LOCAL_*.txt
|
*_LOCAL_*.txt
|
||||||
*_REMOTE_*.txt
|
*_REMOTE_*.txt
|
||||||
|
|
||||||
### Java ###
|
### Java ###
|
||||||
# Compiled class file
|
# Compiled class file
|
||||||
*.class
|
*.class
|
||||||
|
|
||||||
# Log file
|
# Log file
|
||||||
*.log
|
*.log
|
||||||
|
|
||||||
# BlueJ files
|
# BlueJ files
|
||||||
*.ctxt
|
*.ctxt
|
||||||
|
|
||||||
# Mobile Tools for Java (J2ME)
|
# Mobile Tools for Java (J2ME)
|
||||||
.mtj.tmp/
|
.mtj.tmp/
|
||||||
|
|
||||||
# Package Files #
|
# Package Files #
|
||||||
*.jar
|
*.jar
|
||||||
*.war
|
*.war
|
||||||
*.nar
|
*.nar
|
||||||
*.ear
|
*.ear
|
||||||
*.zip
|
*.zip
|
||||||
*.tar.gz
|
*.tar.gz
|
||||||
*.rar
|
*.rar
|
||||||
*.db
|
*.db
|
||||||
/build
|
/build
|
||||||
|
|
||||||
/.vscode
|
/.vscode
|
||||||
/.idea
|
/.idea
|
||||||
|
|
||||||
# Ignore Mac DS_Store files
|
# Ignore Mac DS_Store files
|
||||||
.DS_Store
|
.DS_Store
|
||||||
**/.DS_Store
|
**/.DS_Store
|
||||||
37
.pre-commit-config.yaml
Normal file
37
.pre-commit-config.yaml
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
repos:
|
||||||
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
|
rev: v0.2.1
|
||||||
|
hooks:
|
||||||
|
- id: ruff
|
||||||
|
args:
|
||||||
|
- --fix
|
||||||
|
- --line-length=127
|
||||||
|
files: ^((.github/scripts)/.+)?[^/]+\.py$
|
||||||
|
- id: ruff-format
|
||||||
|
files: ^((.github/scripts)/.+)?[^/]+\.py$
|
||||||
|
- repo: https://github.com/codespell-project/codespell
|
||||||
|
rev: v2.2.6
|
||||||
|
hooks:
|
||||||
|
- id: codespell
|
||||||
|
args:
|
||||||
|
- --ignore-words-list=
|
||||||
|
- --skip="./.*,*.csv,*.json,*.ambr"
|
||||||
|
- --quiet-level=2
|
||||||
|
files: \.(properties|html|css|js|py|md)$
|
||||||
|
exclude: (.vscode|.devcontainer|src/main/resources|Dockerfile)
|
||||||
|
- repo: local
|
||||||
|
hooks:
|
||||||
|
- id: check-duplicate-properties-keys
|
||||||
|
name: Check Duplicate Properties Keys
|
||||||
|
entry: python .github/scripts/check_duplicates.py
|
||||||
|
language: python
|
||||||
|
files: ^(src)/.+\.properties$
|
||||||
|
- repo: local
|
||||||
|
hooks:
|
||||||
|
- id: check-html-tabs
|
||||||
|
name: Check HTML for tabs
|
||||||
|
# args: ["--replace_with= "]
|
||||||
|
entry: python .github/scripts/check_tabulator.py
|
||||||
|
language: python
|
||||||
|
exclude: ^src/main/resources/static/pdfjs/
|
||||||
|
files: ^.*(\.html|\.css|\.js)$
|
||||||
@@ -27,6 +27,10 @@ Please make sure your Pull Request adheres to the following guidelines:
|
|||||||
|
|
||||||
If you would like to add or modify a translation, please see [How to add new languages to Stirling-PDF](HowToAddNewLanguage.md). Also, please create a Pull Request so others can use it!
|
If you would like to add or modify a translation, please see [How to add new languages to Stirling-PDF](HowToAddNewLanguage.md). Also, please create a Pull Request so others can use it!
|
||||||
|
|
||||||
|
## Docs
|
||||||
|
|
||||||
|
Documentation for Stirling-PDF is handled in a seperate repository. Please see [Docs repository](https://github.com/Stirling-Tools/Stirling-Tools.github.io) or use "edit this page"-button at the bottom of each page at [https://stirlingtools.com/docs/](https://stirlingtools.com/docs/).
|
||||||
|
|
||||||
## Fixing Bugs or Adding a New Feature
|
## Fixing Bugs or Adding a New Feature
|
||||||
|
|
||||||
First, make sure you've read the section [Pull Requests](#pull-requests).
|
First, make sure you've read the section [Pull Requests](#pull-requests).
|
||||||
|
|||||||
117
Dockerfile
117
Dockerfile
@@ -1,47 +1,70 @@
|
|||||||
# Use the base image
|
# Main stage
|
||||||
FROM frooodle/stirling-pdf-base:version8
|
FROM alpine:20240329
|
||||||
|
|
||||||
ARG VERSION_TAG
|
# Copy necessary files
|
||||||
|
COPY scripts /scripts
|
||||||
# Set Environment Variables
|
COPY pipeline /pipeline
|
||||||
ENV DOCKER_ENABLE_SECURITY=false \
|
COPY src/main/resources/static/fonts/*.ttf /usr/share/fonts/opentype/noto/
|
||||||
HOME=/home/stirlingpdfuser \
|
#COPY src/main/resources/static/fonts/*.otf /usr/share/fonts/opentype/noto/
|
||||||
VERSION_TAG=$VERSION_TAG \
|
COPY build/libs/*.jar app.jar
|
||||||
JAVA_TOOL_OPTIONS="$JAVA_TOOL_OPTIONS -XX:MaxRAMPercentage=75"
|
|
||||||
# PUID=1000 \
|
ARG VERSION_TAG
|
||||||
# PGID=1000 \
|
|
||||||
# UMASK=022 \
|
|
||||||
|
# Set Environment Variables
|
||||||
|
ENV DOCKER_ENABLE_SECURITY=false \
|
||||||
# Create user and group
|
VERSION_TAG=$VERSION_TAG \
|
||||||
##RUN groupadd -g $PGID stirlingpdfgroup && \
|
JAVA_TOOL_OPTIONS="$JAVA_TOOL_OPTIONS -XX:MaxRAMPercentage=75" \
|
||||||
## useradd -u $PUID -g stirlingpdfgroup -s /bin/sh stirlingpdfuser && \
|
HOME=/home/stirlingpdfuser \
|
||||||
## mkdir -p $HOME && chown stirlingpdfuser:stirlingpdfgroup $HOME
|
PUID=1000 \
|
||||||
|
PGID=1000 \
|
||||||
# Set up necessary directories and permissions
|
UMASK=022
|
||||||
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 && \
|
# JDK for app
|
||||||
## chown -R stirlingpdfuser:stirlingpdfgroup /usr/share/tesseract-ocr-original
|
RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/apk/repositories && \
|
||||||
|
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/community" | tee -a /etc/apk/repositories && \
|
||||||
# Copy necessary files
|
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/testing" | tee -a /etc/apk/repositories && \
|
||||||
COPY ./scripts/* /scripts/
|
apk update && \
|
||||||
COPY ./pipeline/ /pipeline/
|
apk add --no-cache \
|
||||||
COPY src/main/resources/static/fonts/*.ttf /usr/share/fonts/opentype/noto/
|
ca-certificates \
|
||||||
COPY src/main/resources/static/fonts/*.otf /usr/share/fonts/opentype/noto/
|
tzdata \
|
||||||
COPY build/libs/*.jar app.jar
|
tini \
|
||||||
|
openssl \
|
||||||
# Set font cache and permissions
|
openssl-dev \
|
||||||
RUN fc-cache -f -v && chmod +x /scripts/*
|
bash \
|
||||||
|
curl \
|
||||||
##&& \
|
openjdk21-jre \
|
||||||
## chown stirlingpdfuser:stirlingpdfgroup /app.jar && \
|
su-exec \
|
||||||
## chmod +x /scripts/init.sh
|
shadow \
|
||||||
|
# Doc conversion
|
||||||
# Expose necessary ports
|
libreoffice@testing \
|
||||||
EXPOSE 8080
|
# pdftohtml
|
||||||
|
poppler-utils \
|
||||||
# Set user and run command
|
# OCR MY PDF (unpaper for descew and other advanced featues)
|
||||||
##USER stirlingpdfuser
|
ocrmypdf \
|
||||||
ENTRYPOINT ["/scripts/init.sh"]
|
tesseract-ocr-data-eng \
|
||||||
CMD ["java", "-Dfile.encoding=UTF-8", "-jar", "/app.jar"]
|
# CV
|
||||||
|
py3-opencv \
|
||||||
|
# python3/pip
|
||||||
|
python3 && \
|
||||||
|
wget https://bootstrap.pypa.io/get-pip.py -qO - | python3 - --break-system-packages --no-cache-dir --upgrade && \
|
||||||
|
# uno unoconv and HTML
|
||||||
|
pip install --break-system-packages --no-cache-dir --upgrade unoconv WeasyPrint && \
|
||||||
|
mv /usr/share/tessdata /usr/share/tessdata-original && \
|
||||||
|
mkdir -p $HOME /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders && \
|
||||||
|
fc-cache -f -v && \
|
||||||
|
chmod +x /scripts/* && \
|
||||||
|
chmod +x /scripts/init.sh && \
|
||||||
|
# User permissions
|
||||||
|
addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \
|
||||||
|
chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /usr/share/fonts/opentype/noto /configs /customFiles /pipeline && \
|
||||||
|
chown stirlingpdfuser:stirlingpdfgroup /app.jar && \
|
||||||
|
tesseract --list-langs && \
|
||||||
|
rm -rf /var/cache/apk/*
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
# Set user and run command
|
||||||
|
ENTRYPOINT ["tini", "--", "/scripts/init.sh"]
|
||||||
|
CMD ["java", "-Dfile.encoding=UTF-8", "-jar", "/app.jar"]
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
# 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"]
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
# Build jbig2enc in a separate stage
|
# use alpine
|
||||||
FROM bellsoft/liberica-openjdk-alpine:17
|
FROM alpine:3.19.1
|
||||||
|
|
||||||
ARG VERSION_TAG
|
ARG VERSION_TAG
|
||||||
|
|
||||||
@@ -7,40 +7,45 @@ ARG VERSION_TAG
|
|||||||
ENV DOCKER_ENABLE_SECURITY=false \
|
ENV DOCKER_ENABLE_SECURITY=false \
|
||||||
HOME=/home/stirlingpdfuser \
|
HOME=/home/stirlingpdfuser \
|
||||||
VERSION_TAG=$VERSION_TAG \
|
VERSION_TAG=$VERSION_TAG \
|
||||||
JAVA_TOOL_OPTIONS="$JAVA_TOOL_OPTIONS -XX:MaxRAMPercentage=75"
|
JAVA_TOOL_OPTIONS="$JAVA_TOOL_OPTIONS -XX:MaxRAMPercentage=75" \
|
||||||
# PUID=1000 \
|
PUID=1000 \
|
||||||
# PGID=1000 \
|
PGID=1000 \
|
||||||
# UMASK=022 \
|
UMASK=022
|
||||||
|
|
||||||
# Create user and group using Alpine's addgroup and adduser
|
# Copy necessary files
|
||||||
#RUN addgroup -g $PGID stirlingpdfgroup && \
|
COPY scripts/download-security-jar.sh /scripts/download-security-jar.sh
|
||||||
# adduser -u $PUID -G stirlingpdfgroup -s /bin/sh -D stirlingpdfuser && \
|
COPY scripts/init-without-ocr.sh /scripts/init-without-ocr.sh
|
||||||
# mkdir -p $HOME && chown stirlingpdfuser:stirlingpdfgroup $HOME
|
COPY pipeline /pipeline
|
||||||
|
|
||||||
# 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
|
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 && \
|
# Set up necessary directories and permissions
|
||||||
chmod +x /scripts/download-security-jar.sh && \
|
|
||||||
apk add --no-cache curl
|
|
||||||
|
|
||||||
# Expose the application port
|
RUN mkdir /configs /logs /customFiles && \
|
||||||
EXPOSE 8080
|
chmod +x /scripts/*.sh && \
|
||||||
|
apk add --no-cache \
|
||||||
|
ca-certificates \
|
||||||
|
tzdata \
|
||||||
|
tini \
|
||||||
|
bash \
|
||||||
|
curl \
|
||||||
|
su-exec \
|
||||||
|
shadow \
|
||||||
|
openjdk21-jre && \
|
||||||
|
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/apk/repositories && \
|
||||||
|
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/community" | tee -a /etc/apk/repositories && \
|
||||||
|
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/testing" | tee -a /etc/apk/repositories && \
|
||||||
|
# User permissions
|
||||||
|
addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \
|
||||||
|
chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /configs /customFiles /pipeline && \
|
||||||
|
chown stirlingpdfuser:stirlingpdfgroup /app.jar
|
||||||
|
|
||||||
# Set environment variables
|
# Set environment variables
|
||||||
ENV ENDPOINTS_GROUPS_TO_REMOVE=CLI
|
ENV ENDPOINTS_GROUPS_TO_REMOVE=CLI
|
||||||
|
|
||||||
ENTRYPOINT ["/scripts/init-without-ocr.sh"]
|
EXPOSE 8080
|
||||||
|
|
||||||
|
ENTRYPOINT ["tini", "--", "/scripts/init-without-ocr.sh"]
|
||||||
|
|
||||||
# Run the application
|
# Run the application
|
||||||
CMD ["java", "-Dfile.encoding=UTF-8", "-jar", "/app.jar"]
|
CMD ["java", "-Dfile.encoding=UTF-8", "-jar", "/app.jar"]
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
# Main stage
|
|
||||||
FROM ubuntu:latest AS base
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# JDK for app
|
|
||||||
RUN apt-get update && \
|
|
||||||
apt-get install -y --no-install-recommends \
|
|
||||||
openjdk-17-jre && \
|
|
||||||
rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Doc conversion
|
|
||||||
RUN apt-get update && \
|
|
||||||
apt-get install -y --no-install-recommends \
|
|
||||||
libreoffice-core \
|
|
||||||
libreoffice-common \
|
|
||||||
libreoffice-writer \
|
|
||||||
libreoffice-calc \
|
|
||||||
libreoffice-impress \
|
|
||||||
python3-uno \
|
|
||||||
curl \
|
|
||||||
unoconv && \
|
|
||||||
rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
|
|
||||||
# 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 \
|
|
||||||
ocrmypdf \
|
|
||||||
unpaper && \
|
|
||||||
rm -rf /var/lib/apt/lists/* && \
|
|
||||||
mv /usr/share/tesseract-ocr /usr/share/tesseract-ocr-original && \
|
|
||||||
pip install --no-cache-dir --upgrade pip && \
|
|
||||||
pip install --no-cache-dir --upgrade ocrmypdf && \
|
|
||||||
pip install --no-cache-dir --upgrade pillow==10.0.1 reportlab==3.6.13 wheel==0.38.1 setuptools==65.5.1 pyjwt==2.4.0 cryptography==39.0.1
|
|
||||||
|
|
||||||
|
|
||||||
#CV and HTML
|
|
||||||
RUN pip install --no-cache-dir opencv-python-headless WeasyPrint
|
|
||||||
@@ -1,46 +1,46 @@
|
|||||||
| Operation | PageOps | Convert | Security | Other | CLI | Python | OpenCV | LibreOffice | OCRmyPDF | Java | Javascript |
|
| Operation | PageOps | Convert | Security | Other | CLI | Python | OpenCV | LibreOffice | OCRmyPDF | Java | Javascript |
|
||||||
|---------------------|---------|---------|----------|-------|------|--------|--------|-------------|----------|----------|------------|
|
|---------------------|---------|---------|----------|-------|------|--------|--------|-------------|----------|----------|------------|
|
||||||
| adjust-contrast | ✔️ | | | | | | | | | | ✔️ |
|
| adjust-contrast | ✔️ | | | | | | | | | | ✔️ |
|
||||||
| auto-split-pdf | ✔️ | | | | | | | | | ✔️ | |
|
| auto-split-pdf | ✔️ | | | | | | | | | ✔️ | |
|
||||||
| crop | ✔️ | | | | | | | | | ✔️ | |
|
| crop | ✔️ | | | | | | | | | ✔️ | |
|
||||||
| extract-page | ✔️ | | | | | | | | | ✔️ | |
|
| extract-page | ✔️ | | | | | | | | | ✔️ | |
|
||||||
| merge-pdfs | ✔️ | | | | | | | | | ✔️ | |
|
| merge-pdfs | ✔️ | | | | | | | | | ✔️ | |
|
||||||
| multi-page-layout | ✔️ | | | | | | | | | ✔️ | |
|
| multi-page-layout | ✔️ | | | | | | | | | ✔️ | |
|
||||||
| pdf-organizer | ✔️ | | | | | | | | | ✔️ | ✔️ |
|
| pdf-organizer | ✔️ | | | | | | | | | ✔️ | ✔️ |
|
||||||
| pdf-to-single-page | ✔️ | | | | | | | | | ✔️ | |
|
| pdf-to-single-page | ✔️ | | | | | | | | | ✔️ | |
|
||||||
| remove-pages | ✔️ | | | | | | | | | ✔️ | |
|
| remove-pages | ✔️ | | | | | | | | | ✔️ | |
|
||||||
| rotate-pdf | ✔️ | | | | | | | | | ✔️ | |
|
| rotate-pdf | ✔️ | | | | | | | | | ✔️ | |
|
||||||
| scale-pages | ✔️ | | | | | | | | | ✔️ | |
|
| scale-pages | ✔️ | | | | | | | | | ✔️ | |
|
||||||
| split-pdfs | ✔️ | | | | | | | | | ✔️ | |
|
| split-pdfs | ✔️ | | | | | | | | | ✔️ | |
|
||||||
| file-to-pdf | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
| file-to-pdf | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
||||||
| img-to-pdf | | ✔️ | | | | | | | | ✔️ | |
|
| img-to-pdf | | ✔️ | | | | | | | | ✔️ | |
|
||||||
| pdf-to-html | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
| pdf-to-html | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
||||||
| pdf-to-img | | ✔️ | | | | | | | | ✔️ | |
|
| pdf-to-img | | ✔️ | | | | | | | | ✔️ | |
|
||||||
| pdf-to-pdfa | | ✔️ | | | ✔️ | | | | ✔️ | | |
|
| pdf-to-pdfa | | ✔️ | | | ✔️ | | | | ✔️ | | |
|
||||||
| pdf-to-markdown | | ✔️ | | | | | | | | ✔️ | |
|
| pdf-to-markdown | | ✔️ | | | | | | | | ✔️ | |
|
||||||
| pdf-to-presentation | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
| pdf-to-presentation | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
||||||
| pdf-to-text | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
| pdf-to-text | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
||||||
| pdf-to-word | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
| pdf-to-word | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
||||||
| pdf-to-xml | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
| pdf-to-xml | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
||||||
| xlsx-to-pdf | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
| xlsx-to-pdf | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
||||||
| add-password | | | ✔️ | | | | | | | ✔️ | |
|
| add-password | | | ✔️ | | | | | | | ✔️ | |
|
||||||
| add-watermark | | | ✔️ | | | | | | | ✔️ | |
|
| add-watermark | | | ✔️ | | | | | | | ✔️ | |
|
||||||
| cert-sign | | | ✔️ | | | | | | | ✔️ | |
|
| cert-sign | | | ✔️ | | | | | | | ✔️ | |
|
||||||
| change-permissions | | | ✔️ | | | | | | | ✔️ | |
|
| change-permissions | | | ✔️ | | | | | | | ✔️ | |
|
||||||
| remove-password | | | ✔️ | | | | | | | ✔️ | |
|
| remove-password | | | ✔️ | | | | | | | ✔️ | |
|
||||||
| sanitize-pdf | | | ✔️ | | | | | | | ✔️ | |
|
| sanitize-pdf | | | ✔️ | | | | | | | ✔️ | |
|
||||||
| add-image | | | | ✔️ | | | | | | ✔️ | |
|
| add-image | | | | ✔️ | | | | | | ✔️ | |
|
||||||
| add-page-numbers | | | | ✔️ | | | | | | ✔️ | |
|
| add-page-numbers | | | | ✔️ | | | | | | ✔️ | |
|
||||||
| auto-rename | | | | ✔️ | | | | | | ✔️ | |
|
| auto-rename | | | | ✔️ | | | | | | ✔️ | |
|
||||||
| change-metadata | | | | ✔️ | | | | | | ✔️ | |
|
| change-metadata | | | | ✔️ | | | | | | ✔️ | |
|
||||||
| compare | | | | ✔️ | | | | | | | ✔️ |
|
| compare | | | | ✔️ | | | | | | | ✔️ |
|
||||||
| compress-pdf | | | | ✔️ | ✔️ | | | | ✔️ | | |
|
| compress-pdf | | | | ✔️ | ✔️ | | | | ✔️ | | |
|
||||||
| extract-image-scans | | | | ✔️ | ✔️ | ✔️ | ✔️ | | | | |
|
| extract-image-scans | | | | ✔️ | ✔️ | ✔️ | ✔️ | | | | |
|
||||||
| extract-images | | | | ✔️ | | | | | | ✔️ | |
|
| extract-images | | | | ✔️ | | | | | | ✔️ | |
|
||||||
| flatten | | | | ✔️ | | | | | | | ✔️ |
|
| flatten | | | | ✔️ | | | | | | | ✔️ |
|
||||||
| get-info-on-pdf | | | | ✔️ | | | | | | ✔️ | |
|
| get-info-on-pdf | | | | ✔️ | | | | | | ✔️ | |
|
||||||
| ocr-pdf | | | | ✔️ | ✔️ | | | | ✔️ | | |
|
| ocr-pdf | | | | ✔️ | ✔️ | | | | ✔️ | | |
|
||||||
| remove-blanks | | | | ✔️ | ✔️ | ✔️ | ✔️ | | | | |
|
| remove-blanks | | | | ✔️ | ✔️ | ✔️ | ✔️ | | | | |
|
||||||
| repair | | | | ✔️ | ✔️ | | | ✔️ | | | |
|
| repair | | | | ✔️ | ✔️ | | | ✔️ | | | |
|
||||||
| show-javascript | | | | ✔️ | | | | | | | ✔️ |
|
| show-javascript | | | | ✔️ | | | | | | | ✔️ |
|
||||||
| sign | | | | ✔️ | | | | | | | ✔️ |
|
| sign | | | | ✔️ | | | | | | | ✔️ |
|
||||||
@@ -1,13 +1,5 @@
|
|||||||
## User Guide for Local Directory Scanning and File Processing
|
## User Guide for Local Directory Scanning and File Processing
|
||||||
|
|
||||||
### Whilst Pipelines are in alpha...
|
|
||||||
You must enable this alpha functionality by setting
|
|
||||||
```yaml
|
|
||||||
system:
|
|
||||||
enableAlphaFunctionality: true
|
|
||||||
```
|
|
||||||
To true like in the above for your `/config/settings.yml` file, after restarting Stirling-PDF you should see both UI and folder scanning enabled.
|
|
||||||
|
|
||||||
### Setting Up Watched Folders:
|
### Setting Up Watched Folders:
|
||||||
- Create a folder where you want your files to be monitored. This is your 'watched folder'.
|
- Create a folder where you want your files to be monitored. This is your 'watched folder'.
|
||||||
- The default directory for this is `./pipeline/watchedFolders/`
|
- The default directory for this is `./pipeline/watchedFolders/`
|
||||||
|
|||||||
@@ -9,13 +9,13 @@ 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/Stirling-Tools/Stirling-PDF/blob/main/src/main/resources/templates/fragments/languages.html
|
https://github.com/Stirling-Tools/Stirling-PDF/blob/main/src/main/resources/templates/fragments/languages.html
|
||||||
and add a flag svg file to
|
and add a flag svg file to
|
||||||
https://github.com/Stirling-Tools/Stirling-PDF/tree/main/src/main/resources/static/images/flags
|
https://github.com/Stirling-Tools/Stirling-PDF/tree/main/src/main/resources/static/images/flags
|
||||||
Any SVG flags are fine, i got most of mine from [here](https://flagicons.lipis.dev/)
|
Any SVG flags are fine, i got most of mine from [here](https://flagicons.lipis.dev/)
|
||||||
If your language isnt represented by a flag just find whichever closely matches it, such as for Arabic i chose Saudi Arabia
|
If your language isn't represented by a flag just find whichever closely matches it, such as for Arabic i chose Saudi Arabia
|
||||||
|
|
||||||
|
|
||||||
For example to add Polish you would add
|
For example to add Polish you would add
|
||||||
```html
|
```html
|
||||||
<a class="dropdown-item lang_dropdown-item" href="" data-language-code="pl_PL">
|
<a class="dropdown-item lang_dropdown-item" href="" data-language-code="pl_PL">
|
||||||
<img src="images/flags/pl.svg" alt="icon" width="20" height="15"> Polski
|
<img src="images/flags/pl.svg" alt="icon" width="20" height="15"> Polski
|
||||||
@@ -23,7 +23,7 @@ For example to add Polish you would add
|
|||||||
```
|
```
|
||||||
The data-language-code is the code used to reference the file in the next step.
|
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/Stirling-Tools/Stirling-PDF/blob/main/src/main/resources/messages_en_GB.properties](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/src/main/resources/messages_en_GB.properties)
|
[https://github.com/Stirling-Tools/Stirling-PDF/blob/main/src/main/resources/messages_en_GB.properties](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/src/main/resources/messages_en_GB.properties)
|
||||||
|
|
||||||
@@ -32,7 +32,20 @@ Copy and rename it to messages_{your data-language-code here}.properties, in the
|
|||||||
|
|
||||||
Then simply translate all property entries within that file and make a PR into main for others to use!
|
Then simply translate all property entries within that file and make a PR into main for others to use!
|
||||||
|
|
||||||
If you do not have a java IDE i am happy to verify the changes worked once you raise PR (but wont be able to verify the translations themselves)
|
If you do not have a java IDE i am happy to verify the changes worked once you raise PR (but won't be able to verify the translations themselves)
|
||||||
|
|
||||||
|
## Handling Untranslatable Strings
|
||||||
|
|
||||||
|
Sometimes, certain strings in the properties file may not require translation because they are the same in the target language or are universal (like names of protocols, certain terminologies, etc.). To ensure accurate statistics for language progress, these strings should be added to the `ignore_translation.toml` file located in the `scripts` directory. This will exclude them from the translation progress calculations.
|
||||||
|
|
||||||
|
For example, if the English string error=Error does not need translation in Polish, add it to the ignore_translation.toml under the Polish section:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[pl_PL]
|
||||||
|
ignore = [
|
||||||
|
"language.direction", # Existing entries
|
||||||
|
"error" # Add new entries here
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
Make sure to place the entry under the correct language section. This helps maintain the accuracy of translation progress statistics and ensures that the translation tool or scripts do not misinterpret the completion rate.
|
||||||
|
|||||||
@@ -2,12 +2,12 @@
|
|||||||
|
|
||||||
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!
|
## My OCR used to work and now doesn't!
|
||||||
Please update your tesseract docker volume path version from 4.00 to 5
|
The paths have changed for the tessadata locations on new docker images, please use ``/usr/share/tessdata`` (Others should still work for backwards compatibility but might not)
|
||||||
|
|
||||||
## How does the OCR Work
|
## How does the OCR Work
|
||||||
Stirling-PDF uses [OCRmyPDF](https://github.com/ocrmypdf/OCRmyPDF) which in turn uses tesseract for its text recognition.
|
Stirling-PDF uses [OCRmyPDF](https://github.com/ocrmypdf/OCRmyPDF) which in turn uses tesseract for its text recognition.
|
||||||
All credit goes to them for this awesome work!
|
All credit goes to them for this awesome work!
|
||||||
|
|
||||||
## Language Packs
|
## Language Packs
|
||||||
|
|
||||||
@@ -21,13 +21,13 @@ 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/5/tessdata` (Debian) or `/usr/share/tesseract/tessdata` (Fedora)
|
2. Place the `.traineddata` files in the Tesseract tessdata directory: `/usr/share/tessdata`
|
||||||
|
|
||||||
# DO NOT REMOVE EXISTING ENG.TRAINEDDATA, IT'S REQUIRED.
|
# DO NOT REMOVE EXISTING ENG.TRAINEDDATA, IT'S REQUIRED.
|
||||||
|
|
||||||
#### Docker
|
#### Docker
|
||||||
|
|
||||||
If you are using Docker, you need to expose the Tesseract tessdata directory as a volume in order to use the additional language packs.
|
If you are using Docker, you need to expose the Tesseract tessdata directory as a volume in order to use the additional language packs.
|
||||||
#### Docker Compose
|
#### Docker Compose
|
||||||
Modify your `docker-compose.yml` file to include the following volume configuration:
|
Modify your `docker-compose.yml` file to include the following volume configuration:
|
||||||
|
|
||||||
@@ -37,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/5/tessdata
|
- /location/of/trainingData:/usr/share/tessdata
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
#### Docker run
|
#### Docker run
|
||||||
Add the following to your existing docker run command
|
Add the following to your existing docker run command
|
||||||
```bash
|
```bash
|
||||||
-v /location/of/trainingData:/usr/share/tesseract-ocr/5/tessdata
|
-v /location/of/trainingData:/usr/share/tessdata
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Non-Docker
|
#### Non-Docker
|
||||||
|
|||||||
88
Jenkinsfile
vendored
88
Jenkinsfile
vendored
@@ -1,45 +1,45 @@
|
|||||||
pipeline {
|
pipeline {
|
||||||
agent any
|
agent any
|
||||||
stages {
|
stages {
|
||||||
stage('Build') {
|
stage('Build') {
|
||||||
steps {
|
steps {
|
||||||
sh 'chmod 755 gradlew'
|
sh 'chmod 755 gradlew'
|
||||||
sh './gradlew build'
|
sh './gradlew build'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
stage('Docker Build') {
|
stage('Docker Build') {
|
||||||
steps {
|
steps {
|
||||||
script {
|
script {
|
||||||
def appVersion = sh(returnStdout: true, script: './gradlew printVersion -q').trim()
|
def appVersion = sh(returnStdout: true, script: './gradlew printVersion -q').trim()
|
||||||
def image = "frooodle/s-pdf:$appVersion"
|
def image = "frooodle/s-pdf:$appVersion"
|
||||||
sh "docker build -t $image ."
|
sh "docker build -t $image ."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
stage('Docker Push') {
|
stage('Docker Push') {
|
||||||
steps {
|
steps {
|
||||||
script {
|
script {
|
||||||
def appVersion = sh(returnStdout: true, script: './gradlew printVersion -q').trim()
|
def appVersion = sh(returnStdout: true, script: './gradlew printVersion -q').trim()
|
||||||
def image = "frooodle/s-pdf:$appVersion"
|
def image = "frooodle/s-pdf:$appVersion"
|
||||||
withCredentials([string(credentialsId: 'docker_hub_access_token', variable: 'DOCKER_HUB_ACCESS_TOKEN')]) {
|
withCredentials([string(credentialsId: 'docker_hub_access_token', variable: 'DOCKER_HUB_ACCESS_TOKEN')]) {
|
||||||
sh "docker login --username frooodle --password $DOCKER_HUB_ACCESS_TOKEN"
|
sh "docker login --username frooodle --password $DOCKER_HUB_ACCESS_TOKEN"
|
||||||
sh "docker push $image"
|
sh "docker push $image"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
stage('Helm Push') {
|
stage('Helm Push') {
|
||||||
steps {
|
steps {
|
||||||
script {
|
script {
|
||||||
//TODO: Read chartVersion from Chart.yaml
|
//TODO: Read chartVersion from Chart.yaml
|
||||||
def chartVersion = '1.0.0'
|
def chartVersion = '1.0.0'
|
||||||
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 "helm package chart/stirling-pdf"
|
sh "helm package chart/stirling-pdf"
|
||||||
sh "helm push stirling-pdf-chart-1.0.0.tgz oci://registry-1.docker.io/frooodle"
|
sh "helm push stirling-pdf-chart-1.0.0.tgz oci://registry-1.docker.io/frooodle"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -14,7 +14,7 @@ You could theoretically use a Distrobox/Toolbox, if your Distribution has old or
|
|||||||
|
|
||||||
Install the following software, if not already installed:
|
Install the following software, if not already installed:
|
||||||
|
|
||||||
- Java 17 or later
|
- Java 17 or later (21 recommended)
|
||||||
|
|
||||||
- Gradle 7.0 or later (included within repo so not needed on server)
|
- Gradle 7.0 or later (included within repo so not needed on server)
|
||||||
|
|
||||||
@@ -42,17 +42,25 @@ For Debian-based systems, you can use the following command:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y git automake autoconf libtool libleptonica-dev pkg-config zlib1g-dev make g++ java-17-openjdk python3 python3-pip
|
sudo apt-get install -y git automake autoconf libtool libleptonica-dev pkg-config zlib1g-dev make g++ openjdk-21-jdk python3 python3-pip
|
||||||
```
|
```
|
||||||
|
|
||||||
For Fedora-based systems use this command:
|
For Fedora-based systems use this command:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo dnf install -y git automake autoconf libtool leptonica-devel pkg-config zlib-devel make gcc-c++ java-17-openjdk python3 python3-pip
|
sudo dnf install -y git automake autoconf libtool leptonica-devel pkg-config zlib-devel make gcc-c++ java-21-openjdk python3 python3-pip
|
||||||
|
```
|
||||||
|
|
||||||
|
For non-root users with Nix Package Manager, use the following command:
|
||||||
|
```bash
|
||||||
|
nix-channel --update
|
||||||
|
nix-env -iA nixpkgs.jdk21 nixpkgs.git nixpkgs.python38 nixpkgs.gnumake nixpkgs.libgcc nixpkgs.automake nixpkgs.autoconf nixpkgs.libtool nixpkgs.pkg-config nixpkgs.zlib nixpkgs.leptonica
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 2: Clone and Build jbig2enc (Only required for certain OCR functionality)
|
### Step 2: Clone and Build jbig2enc (Only required for certain OCR functionality)
|
||||||
|
|
||||||
|
For Debian and Fedora, you can build it from source using the following commands:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mkdir ~/.git
|
mkdir ~/.git
|
||||||
cd ~/.git &&\
|
cd ~/.git &&\
|
||||||
@@ -64,8 +72,13 @@ make &&\
|
|||||||
sudo make install
|
sudo make install
|
||||||
```
|
```
|
||||||
|
|
||||||
|
For Nix, you will face `Leptonica not detected`. Bypass this by installing it directly using the following command:
|
||||||
|
```bash
|
||||||
|
nix-env -iA nixpkgs.jbig2enc
|
||||||
|
```
|
||||||
|
|
||||||
### Step 3: Install Additional Software
|
### Step 3: Install Additional Software
|
||||||
Next we need to install LibreOffice for conversions, ocrmypdf for OCR, and opencv for patern recognition functionality.
|
Next we need to install LibreOffice for conversions, ocrmypdf for OCR, and opencv for pattern recognition functionality.
|
||||||
|
|
||||||
Install the following software:
|
Install the following software:
|
||||||
|
|
||||||
@@ -95,14 +108,21 @@ For Debian-based systems, you can use the following command:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo apt-get install -y libreoffice-writer libreoffice-calc libreoffice-impress unpaper ocrmypdf
|
sudo apt-get install -y libreoffice-writer libreoffice-calc libreoffice-impress unpaper ocrmypdf
|
||||||
pip3 install uno opencv-python-headless unoconv pngquant WeasyPrint
|
pip3 install uno opencv-python-headless unoconv pngquant WeasyPrint --break-system-packages
|
||||||
```
|
```
|
||||||
|
|
||||||
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 WeasyPrint
|
pip3 install uno opencv-python-headless unoconv pngquant WeasyPrint
|
||||||
|
```
|
||||||
|
|
||||||
|
For Nix:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nix-env -iA nixpkgs.unpaper nixpkgs.libreoffice nixpkgs.ocrmypdf nixpkgs.poppler_utils
|
||||||
|
pip3 install uno opencv-python-headless unoconv pngquant WeasyPrint
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 4: Clone and Build Stirling-PDF
|
### Step 4: Clone and Build Stirling-PDF
|
||||||
@@ -115,13 +135,12 @@ chmod +x ./gradlew &&\
|
|||||||
./gradlew build
|
./gradlew build
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
### Step 5: Move jar to desired location
|
### Step 5: Move jar to desired location
|
||||||
|
|
||||||
After the build process, a `.jar` file will be generated in the `build/libs` directory.
|
After the build process, a `.jar` file will be generated in the `build/libs` directory.
|
||||||
You can move this file to a desired location, for example, `/opt/Stirling-PDF/`.
|
You can move this file to a desired location, for example, `/opt/Stirling-PDF/`.
|
||||||
You must also move the Script folder within the Stirling-PDF repo that you have downloaded to this directory.
|
You must also move the Script folder within the Stirling-PDF repo that you have downloaded to this directory.
|
||||||
This folder is required for the python scripts using OpenCV
|
This folder is required for the python scripts using OpenCV.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo mkdir /opt/Stirling-PDF &&\
|
sudo mkdir /opt/Stirling-PDF &&\
|
||||||
@@ -129,19 +148,25 @@ 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."
|
||||||
```
|
```
|
||||||
|
|
||||||
|
For non-root users, you can just keep the jar in the main directory of Stirling-PDF using the following command:
|
||||||
|
```bash
|
||||||
|
mv ./build/libs/Stirling-PDF-*.jar ./Stirling-PDF-*.jar
|
||||||
|
```
|
||||||
|
|
||||||
### Step 6: Other files
|
### Step 6: Other files
|
||||||
#### OCR
|
#### OCR
|
||||||
If you plan to use the OCR (Optical Character Recognition) functionality, you might need to install language packs for Tesseract if running non-english scanning.
|
If you plan to use the OCR (Optical Character Recognition) functionality, you might need to install language packs for Tesseract if running non-english scanning.
|
||||||
|
|
||||||
##### Installing Language Packs
|
##### Installing Language Packs
|
||||||
Easiest is to use the langpacks provided by your repositories. Skip the other steps
|
Easiest is to use the langpacks provided by your repositories. Skip the other steps.
|
||||||
|
|
||||||
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/5/tessdata`
|
2. Place the `.traineddata` files in the Tesseract tessdata directory: `/usr/share/tessdata`
|
||||||
3.
|
3. Please view [OCRmyPDF install guide](https://ocrmypdf.readthedocs.io/en/latest/installation.html) for more info.
|
||||||
Please view [OCRmyPDF install guide](https://ocrmypdf.readthedocs.io/en/latest/installation.html) for more info.
|
|
||||||
**IMPORTANT:** DO NOT REMOVE EXISTING `eng.traineddata`, IT'S REQUIRED.
|
**IMPORTANT:** DO NOT REMOVE EXISTING `eng.traineddata`, IT'S REQUIRED.
|
||||||
|
|
||||||
Debian based systems, install languages with this command:
|
Debian based systems, install languages with this command:
|
||||||
@@ -171,14 +196,38 @@ dnf search -C tesseract-langpack-
|
|||||||
rpm -qa | grep tesseract-langpack | sed 's/tesseract-langpack-//g'
|
rpm -qa | grep tesseract-langpack | sed 's/tesseract-langpack-//g'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Nix:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nix-env -iA nixpkgs.tesseract
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** Nix Package Manager pre-installs almost all the language packs when tesseract is installed.
|
||||||
|
|
||||||
### Step 7: Run Stirling-PDF
|
### Step 7: Run Stirling-PDF
|
||||||
|
|
||||||
|
Those who have pushed to the root directory, run the following commands:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./gradlew bootRun
|
./gradlew bootRun
|
||||||
or
|
or
|
||||||
java -jar /opt/Stirling-PDF/Stirling-PDF-*.jar
|
java -jar /opt/Stirling-PDF/Stirling-PDF-*.jar
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Since libreoffice, soffice, and conversion tools have their dbus_tmp_dir set as `dbus_tmp_dir="/run/user/$(id -u)/libreoffice-dbus"`, you might get the following error when using their endpoints:
|
||||||
|
```
|
||||||
|
[Thread-7] INFO s.s.SPDF.utils.ProcessExecutor - mkdir: cannot create directory ‘/run/user/1501’: Permission denied
|
||||||
|
```
|
||||||
|
To resolve this, before starting the Stirling-PDF, you have to set the environment variable to a directory you have write access to by using the following commands:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir temp
|
||||||
|
export DBUS_SESSION_BUS_ADDRESS="unix:path=./temp"
|
||||||
|
./gradlew bootRun
|
||||||
|
or
|
||||||
|
java -jar ./Stirling-PDF-*.jar
|
||||||
|
```
|
||||||
|
|
||||||
### Step 8: Adding a Desktop icon
|
### Step 8: Adding a Desktop icon
|
||||||
|
|
||||||
This will add a modified Appstarter to your Appmenu.
|
This will add a modified Appstarter to your Appmenu.
|
||||||
@@ -202,7 +251,19 @@ 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
|
### Optional: Changing the host and port of the application:
|
||||||
|
|
||||||
|
To override the default configuration, you can add the following to `/.git/Stirling-PDF/configs/custom_settings.yml` file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
server:
|
||||||
|
host: 0.0.0.0
|
||||||
|
port: 3000
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** This file is created after the first application launch. To have it before that, you can create the directory and add the file yourself.
|
||||||
|
|
||||||
|
### Optional: Run Stirling-PDF as a service (requires root).
|
||||||
|
|
||||||
First create a .env file, where you can store environment variables:
|
First create a .env file, where you can store environment variables:
|
||||||
```
|
```
|
||||||
@@ -239,6 +300,7 @@ 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):
|
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
|
sudo systemctl daemon-reload
|
||||||
```
|
```
|
||||||
@@ -264,10 +326,10 @@ 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.
|
||||||
|
|
||||||
You can do this in the terminal by using the `export` command or -D arguements to java -jar command:
|
You can do this in the terminal by using the `export` command or -D argument to java -jar command:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
export APP_HOME_NAME="Stirling PDF"
|
export APP_HOME_NAME="Stirling PDF"
|
||||||
or
|
or
|
||||||
-DAPP_HOME_NAME="Stirling PDF"
|
-DAPP_HOME_NAME="Stirling PDF"
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,13 +1,6 @@
|
|||||||
# Pipeline Configuration and Usage Tutorial
|
# Pipeline Configuration and Usage Tutorial
|
||||||
|
- Configure the pipeline config file and input files to run files against it
|
||||||
## Whilst Pipelines are in alpha...
|
- For reuse, download the config file and re-upload it when needed, or place it in /pipeline/defaultWebUIConfigs/ to auto-load in the web UI for all users
|
||||||
You must enable this alpha functionality by setting
|
|
||||||
```yaml
|
|
||||||
system:
|
|
||||||
enableAlphaFunctionality: true
|
|
||||||
```
|
|
||||||
To true like in the above for your `/config/settings.yml` file, after restarting Stirling-PDF you should see both UI and folder scanning enabled.
|
|
||||||
|
|
||||||
|
|
||||||
## Steps to Configure and Use Your Pipeline
|
## Steps to Configure and Use Your Pipeline
|
||||||
|
|
||||||
@@ -40,3 +33,12 @@ To true like in the above for your `/config/settings.yml` file, after restarting
|
|||||||
|
|
||||||
10. **Note on Web UI Limitations**
|
10. **Note on Web UI Limitations**
|
||||||
- The current web UI version does not support operations that require multiple different types of inputs, such as adding a separate image to a PDF.
|
- The current web UI version does not support operations that require multiple different types of inputs, such as adding a separate image to a PDF.
|
||||||
|
|
||||||
|
|
||||||
|
### Current Limitations
|
||||||
|
- Cannot have more than one of the same operation
|
||||||
|
- Cannot input additional files via UI
|
||||||
|
- All files and operations run in serial mode
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
154
README.md
154
README.md
@@ -1,34 +1,35 @@
|
|||||||
<p align="center"><img src="https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/main/docs/stirling.png" width="80" ><br><h1 align="center">Stirling-PDF</h1>
|
<p align="center"><img src="https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/main/docs/stirling.png" width="80" ></p>
|
||||||
</p>
|
<h1 align="center">Stirling-PDF</h1>
|
||||||
|
|
||||||
[](https://hub.docker.com/r/frooodle/s-pdf)
|
[](https://hub.docker.com/r/frooodle/s-pdf)
|
||||||
[](https://discord.gg/Cn8pWhQRxZ)
|
[](https://discord.gg/Cn8pWhQRxZ)
|
||||||
[](https://github.com/Stirling-Tools/Stirling-PDF/)
|
[](https://github.com/Stirling-Tools/Stirling-PDF/)
|
||||||
[](https://github.com/Stirling-Tools/stirling-pdf)
|
[](https://github.com/Stirling-Tools/stirling-pdf)
|
||||||
[](https://www.paypal.com/paypalme/froodleplex)
|
[](https://www.paypal.com/paypalme/froodleplex)
|
||||||
[](https://github.com/sponsors/Frooodle)
|
[](https://github.com/sponsors/Frooodle)
|
||||||
|
|
||||||
[](https://cloud.digitalocean.com/apps/new?repo=https://github.com/Stirling-Tools/Stirling-PDF/tree/digitalOcean&refcode=c3210994b1af)
|
[](https://cloud.digitalocean.com/apps/new?repo=https://github.com/Stirling-Tools/Stirling-PDF/tree/digitalOcean&refcode=c3210994b1af)
|
||||||
|
|
||||||
This is a powerful locally hosted web based PDF manipulation tool using docker that allows you to perform various operations on PDF files, such as splitting merging, converting, reorganizing, adding images, rotating, compressing, and more. This locally hosted web application started as a 100% ChatGPT-made application and has evolved to include a wide range of features to handle all your PDF needs.
|
This is a robust, locally hosted web-based PDF manipulation tool using Docker. It enables you to carry out various operations on PDF files, including splitting, merging, converting, reorganizing, adding images, rotating, compressing, and more. This locally hosted web application has evolved to encompass a comprehensive set of features, addressing all your PDF requirements.
|
||||||
|
|
||||||
Stirling PDF makes no outbound calls for any record keeping or tracking.
|
Stirling PDF does not initiate any outbound calls for record-keeping or tracking purposes.
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Dark mode support.
|
- Dark mode support.
|
||||||
- Custom download options (see [here](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/images/settings.png) for example)
|
- Custom download options (see [here](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/images/settings.png) for example)
|
||||||
- Parallel file processing and downloads
|
- Parallel file processing and downloads
|
||||||
- API for integration with external scripts
|
- API for integration with external scripts
|
||||||
- Optional Login and Authentication support (see [here](https://github.com/Stirling-Tools/Stirling-PDF/tree/main#login-authentication) for documentation)
|
- Optional Login and Authentication support (see [here](https://github.com/Stirling-Tools/Stirling-PDF/tree/main#login-authentication) for documentation)
|
||||||
|
|
||||||
|
|
||||||
## **PDF Features**
|
## **PDF Features**
|
||||||
|
|
||||||
### **Page Operations**
|
### **Page Operations**
|
||||||
|
|
||||||
- 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)
|
- 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.
|
- Full interactive GUI for merging/splitting/rotating/moving PDFs and their pages.
|
||||||
- Merge multiple PDFs together into a single resultant file.
|
- Merge multiple PDFs together into a single resultant file.
|
||||||
@@ -45,6 +46,7 @@ All files and PDFs exist either exclusively on the client side, reside in server
|
|||||||
- Convert PDF to a single page.
|
- Convert PDF to a single page.
|
||||||
|
|
||||||
### **Conversion Operations**
|
### **Conversion Operations**
|
||||||
|
|
||||||
- Convert PDFs to and from images.
|
- Convert PDFs to and from images.
|
||||||
- Convert any common file to PDF (using LibreOffice).
|
- Convert any common file to PDF (using LibreOffice).
|
||||||
- Convert PDF to Word/Powerpoint/Others (using LibreOffice).
|
- Convert PDF to Word/Powerpoint/Others (using LibreOffice).
|
||||||
@@ -53,6 +55,7 @@ All files and PDFs exist either exclusively on the client side, reside in server
|
|||||||
- Markdown to PDF.
|
- Markdown to PDF.
|
||||||
|
|
||||||
### **Security & Permissions**
|
### **Security & Permissions**
|
||||||
|
|
||||||
- Add and remove passwords.
|
- Add and remove passwords.
|
||||||
- Change/set PDF Permissions.
|
- Change/set PDF Permissions.
|
||||||
- Add watermark(s).
|
- Add watermark(s).
|
||||||
@@ -61,6 +64,7 @@ All files and PDFs exist either exclusively on the client side, reside in server
|
|||||||
- Auto-redact text.
|
- Auto-redact text.
|
||||||
|
|
||||||
### **Other Operations**
|
### **Other Operations**
|
||||||
|
|
||||||
- Add/Generate/Write signatures.
|
- Add/Generate/Write signatures.
|
||||||
- Repair PDFs.
|
- Repair PDFs.
|
||||||
- Detect and remove blank pages.
|
- Detect and remove blank pages.
|
||||||
@@ -77,11 +81,11 @@ All files and PDFs exist either exclusively on the client side, reside in server
|
|||||||
- Flatten PDFs.
|
- Flatten PDFs.
|
||||||
- Get all information on a PDF to view or export as JSON.
|
- Get all information on a PDF to view or export as JSON.
|
||||||
|
|
||||||
|
|
||||||
For a overview of the tasks and the technology each uses please view [Endpoint-groups.md](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/Endpoint-groups.md)
|
For a overview of the tasks and the technology each uses please view [Endpoint-groups.md](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/Endpoint-groups.md)
|
||||||
Demo of the app is available [here](https://stirlingpdf.io). username: demo, password: demo
|
Demo of the app is available [here](https://stirlingpdf.io). username: demo, password: demo
|
||||||
|
|
||||||
## Technologies used
|
## Technologies used
|
||||||
|
|
||||||
- Spring Boot + Thymeleaf
|
- Spring Boot + Thymeleaf
|
||||||
- [PDFBox](https://github.com/apache/pdfbox/tree/trunk)
|
- [PDFBox](https://github.com/apache/pdfbox/tree/trunk)
|
||||||
- [LibreOffice](https://www.libreoffice.org/discover/libreoffice/) for advanced conversions
|
- [LibreOffice](https://www.libreoffice.org/discover/libreoffice/) for advanced conversions
|
||||||
@@ -94,26 +98,30 @@ Demo of the app is available [here](https://stirlingpdf.io). username: demo, pas
|
|||||||
## How to use
|
## How to use
|
||||||
|
|
||||||
### Locally
|
### Locally
|
||||||
|
|
||||||
Please view https://github.com/Stirling-Tools/Stirling-PDF/blob/main/LocalRunGuide.md
|
Please view https://github.com/Stirling-Tools/Stirling-PDF/blob/main/LocalRunGuide.md
|
||||||
|
|
||||||
### Docker / Podman
|
### Docker / Podman
|
||||||
|
|
||||||
https://hub.docker.com/r/frooodle/s-pdf
|
https://hub.docker.com/r/frooodle/s-pdf
|
||||||
|
|
||||||
Stirling PDF has 3 different versions, a Full version, Lite, and ultra-Lite. Depending on the types of features you use you may want a smaller image to save on space.
|
Stirling PDF has 2 different versions, a Full version and ultra-Lite version. 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/Stirling-Tools/Stirling-PDF/blob/main/Version-groups.md)
|
To see what the different versions offer please look at our [version mapping](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/Version-groups.md)
|
||||||
For people that don't mind about space optimization just use the latest tag.
|
For people that don't mind about space optimization just use the latest tag.
|
||||||

|

|
||||||

|
|
||||||

|

|
||||||
|
|
||||||
Docker Run
|
Docker Run
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run -d \
|
docker run -d \
|
||||||
-p 8080:8080 \
|
-p 8080:8080 \
|
||||||
-v /location/of/trainingData:/usr/share/tesseract-ocr/5/tessdata \
|
-v /location/of/trainingData:/usr/share/tessdata \
|
||||||
-v /location/of/extraConfigs:/configs \
|
-v /location/of/extraConfigs:/configs \
|
||||||
-v /location/of/logs:/logs \
|
-v /location/of/logs:/logs \
|
||||||
-e DOCKER_ENABLE_SECURITY=false \
|
-e DOCKER_ENABLE_SECURITY=false \
|
||||||
|
-e INSTALL_BOOK_AND_ADVANCED_HTML_OPS=false \
|
||||||
|
-e LANGS=en_GB \
|
||||||
--name stirling-pdf \
|
--name stirling-pdf \
|
||||||
frooodle/s-pdf:latest
|
frooodle/s-pdf:latest
|
||||||
|
|
||||||
@@ -122,7 +130,9 @@ docker run -d \
|
|||||||
|
|
||||||
-v /location/of/customFiles:/customFiles \
|
-v /location/of/customFiles:/customFiles \
|
||||||
```
|
```
|
||||||
|
|
||||||
Docker Compose
|
Docker Compose
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
version: '3.3'
|
version: '3.3'
|
||||||
services:
|
services:
|
||||||
@@ -131,65 +141,75 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- '8080:8080'
|
- '8080:8080'
|
||||||
volumes:
|
volumes:
|
||||||
- /location/of/trainingData:/usr/share/tesseract-ocr/5/tessdata #Required for extra OCR languages
|
- /location/of/trainingData:/usr/share/tessdata #Required for extra OCR languages
|
||||||
- /location/of/extraConfigs:/configs
|
- /location/of/extraConfigs:/configs
|
||||||
# - /location/of/customFiles:/customFiles/
|
# - /location/of/customFiles:/customFiles/
|
||||||
# - /location/of/logs:/logs/
|
# - /location/of/logs:/logs/
|
||||||
environment:
|
environment:
|
||||||
- DOCKER_ENABLE_SECURITY=false
|
- DOCKER_ENABLE_SECURITY=false
|
||||||
|
- INSTALL_BOOK_AND_ADVANCED_HTML_OPS=false
|
||||||
|
- LANGS=en_GB
|
||||||
```
|
```
|
||||||
|
|
||||||
Note: Podman is CLI-compatible with Docker, so simply replace "docker" with "podman".
|
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/Stirling-Tools/Stirling-PDF/blob/main/HowToUseOCR.md
|
Please view https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToUseOCR.md
|
||||||
|
|
||||||
## Supported Languages
|
## Supported Languages
|
||||||
|
|
||||||
Stirling PDF currently supports 26!
|
Stirling PDF currently supports 27!
|
||||||
- English (English) (en_GB)
|
|
||||||
- English (US) (en_US)
|
| Language | Progress |
|
||||||
- Arabic (العربية) (ar_AR)
|
| ------------------------------------------- | -------------------------------------- |
|
||||||
- German (Deutsch) (de_DE)
|
| English (English) (en_GB) |  |
|
||||||
- French (Français) (fr_FR)
|
| English (US) (en_US) |  |
|
||||||
- Spanish (Español) (es_ES)
|
| Arabic (العربية) (ar_AR) |  |
|
||||||
- Simplified Chinese (简体中文) (zh_CN)
|
| German (Deutsch) (de_DE) |  |
|
||||||
- Traditional Chinese (繁體中文) (zh_TW)
|
| French (Français) (fr_FR) |  |
|
||||||
- Catalan (Català) (ca_CA)
|
| Spanish (Español) (es_ES) |  |
|
||||||
- Italian (Italiano) (it_IT)
|
| Simplified Chinese (简体中文) (zh_CN) |  |
|
||||||
- Swedish (Svenska) (sv_SE)
|
| Traditional Chinese (繁體中文) (zh_TW) |  |
|
||||||
- Polish (Polski) (pl_PL)
|
| Catalan (Català) (ca_CA) |  |
|
||||||
- Romanian (Română) (ro_RO)
|
| Italian (Italiano) (it_IT) |  |
|
||||||
- Korean (한국어) (ko_KR)
|
| Swedish (Svenska) (sv_SE) |  |
|
||||||
- Portuguese Brazilian (Português) (pt_BR)
|
| Polish (Polski) (pl_PL) |  |
|
||||||
- Russian (Русский) (ru_RU)
|
| Romanian (Română) (ro_RO) |  |
|
||||||
- Basque (Euskara) (eu_ES)
|
| Korean (한국어) (ko_KR) |  |
|
||||||
- Japanese (日本語) (ja_JP)
|
| Portuguese Brazilian (Português) (pt_BR) |  |
|
||||||
- Dutch (Nederlands) (nl_NL)
|
| Russian (Русский) (ru_RU) |  |
|
||||||
- Greek (el_GR)
|
| Basque (Euskara) (eu_ES) |  |
|
||||||
- Turkish (Türkçe) (tr_TR)
|
| Japanese (日本語) (ja_JP) |  |
|
||||||
- Indonesia (Bahasa Indonesia) (id_ID)
|
| Dutch (Nederlands) (nl_NL) |  |
|
||||||
- Hindi (हिंदी) (hi_IN)
|
| Greek (Ελληνικά) (el_GR) |  |
|
||||||
- Hungarian (Magyar) (hu_HU)
|
| Turkish (Türkçe) (tr_TR) |  |
|
||||||
- Bulgarian (Български) (bg_BG)
|
| Indonesia (Bahasa Indonesia) (id_ID) |  |
|
||||||
- Sebian Latin alphabet (Srpski) (sr-Latn-RS)
|
| Hindi (हिंदी) (hi_IN) |  |
|
||||||
|
| Hungarian (Magyar) (hu_HU) |  |
|
||||||
|
| Bulgarian (Български) (bg_BG) |  |
|
||||||
|
| Sebian Latin alphabet (Srpski) (sr_LATN_RS) |  |
|
||||||
|
| Ukrainian (Українська) (uk_UA) |  |
|
||||||
|
| Slovakian (Slovensky) (sk_SK) |  |
|
||||||
|
|
||||||
## Contributing (creating issues, translations, fixing bugs, etc.)
|
## Contributing (creating issues, translations, fixing bugs, etc.)
|
||||||
|
|
||||||
Please see our [Contributing Guide](CONTRIBUTING.md)!
|
Please see our [Contributing Guide](CONTRIBUTING.md)!
|
||||||
|
|
||||||
## Customisation
|
## Customisation
|
||||||
|
|
||||||
Stirling PDF allows easy customization of the app.
|
Stirling PDF allows easy customization of the app.
|
||||||
Includes things like
|
Includes things like
|
||||||
- Custom application name
|
|
||||||
- Custom slogans, icons, images, and even custom HTML (via file overrides)
|
|
||||||
|
|
||||||
|
- Custom application name
|
||||||
|
- Custom slogans, icons, HTML, images CSS etc (via file overrides)
|
||||||
|
|
||||||
There are two options for this, either using the generated settings file ``settings.yml``
|
There are two options for this, either using the generated settings file ``settings.yml``
|
||||||
This file is located in the ``/configs`` directory and follows standard YAML formatting
|
This file is located in the ``/configs`` directory and follows standard YAML formatting
|
||||||
|
|
||||||
Environment variables are also supported and would override the settings file
|
Environment variables are also supported and would override the settings file
|
||||||
For example in the settings.yml you have
|
For example in the settings.yml you have
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
system:
|
system:
|
||||||
defaultLocale: 'en-US'
|
defaultLocale: 'en-US'
|
||||||
@@ -198,47 +218,75 @@ system:
|
|||||||
To have this via an environment variable you would have ``SYSTEM_DEFAULTLOCALE``
|
To have this via an environment variable you would have ``SYSTEM_DEFAULTLOCALE``
|
||||||
|
|
||||||
The Current list of settings is
|
The Current list of settings is
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
security:
|
security:
|
||||||
enableLogin: false # set to 'true' to enable login
|
enableLogin: false # set to 'true' to enable login
|
||||||
csrfDisabled: true
|
csrfDisabled: true # Set to 'true' to disable CSRF protection (not recommended for production)
|
||||||
|
loginAttemptCount: 5 # lock user account after 5 tries
|
||||||
|
loginResetTimeMinutes : 120 # lock account for 2 hours after x attempts
|
||||||
|
# initialLogin:
|
||||||
|
# username: "admin" # Initial username for the first login (these are defaulted)
|
||||||
|
# password: "stirling" # Initial password for the first login
|
||||||
|
# oauth2:
|
||||||
|
# enabled: false # set to 'true' to enable login (Note: enableLogin must also be 'true' for this to work)
|
||||||
|
# issuer: "" # set to any provider that supports OpenID Connect Discovery (/.well-known/openid-configuration) end-point
|
||||||
|
# clientId: "" # Client ID from your provider
|
||||||
|
# clientSecret: "" # Client Secret from your provider
|
||||||
|
# autoCreateUser: false # set to 'true' to allow auto-creation of non-existing users
|
||||||
|
# useAsUsername: "email" # Default is 'email'; custom fields can be used as the username
|
||||||
|
# scopes: "openid, profile, email" # Specify the scopes for which the application will request permissions
|
||||||
|
# provider: "google" # Set this to your OAuth provider's name, e.g., 'google' or 'keycloak'
|
||||||
|
|
||||||
system:
|
system:
|
||||||
defaultLocale: 'en-US' # Set the default language (e.g. 'de-DE', 'fr-FR', etc)
|
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
|
googlevisibility: false # 'true' to allow Google visibility (via robots.txt), 'false' to disallow
|
||||||
customStaticFilePath: '/customFiles/static/' # Directory path for custom static files
|
enableAlphaFunctionality: false # Set to enable functionality which might need more testing before it fully goes live (This feature might make no changes)
|
||||||
|
showUpdate: true # see when a new update is available
|
||||||
|
showUpdateOnlyAdmin: false # Only admins can see when a new update is available, depending on showUpdate it must be set to 'true'
|
||||||
|
customHTMLFiles: false # enable to have files placed in /customFiles/templates override the existing template html files
|
||||||
|
|
||||||
#ui:
|
ui:
|
||||||
# appName: exampleAppName # Application's visible name
|
appName: null # Application's visible name
|
||||||
# homeDescription: I am a description # Short description or tagline shown on homepage.
|
homeDescription: null # Short description or tagline shown on homepage.
|
||||||
# appNameNavbar: navbarName # Name displayed on the navigation bar
|
appNameNavbar: null # Name displayed on the navigation bar
|
||||||
|
|
||||||
endpoints:
|
endpoints:
|
||||||
toRemove: [] # List endpoints to disable (e.g. ['img-to-pdf', 'remove-pages'])
|
toRemove: [] # List endpoints to disable (e.g. ['img-to-pdf', 'remove-pages'])
|
||||||
groupsToRemove: [] # List groups to disable (e.g. ['LibreOffice'])
|
groupsToRemove: [] # List groups to disable (e.g. ['LibreOffice'])
|
||||||
|
|
||||||
metrics:
|
metrics:
|
||||||
enabled: true # 'true' to enable Info APIs endpoints (view http://localhost:8080/swagger-ui/index.html#/API to learn more), 'false' to disable
|
enabled: true # 'true' to enable Info APIs (`/api/*`) endpoints, 'false' to disable
|
||||||
```
|
```
|
||||||
|
|
||||||
|
There is an additional config file ``/configs/custom_settings.yml`` were users familiar with java and spring application.properties can input their own settings on-top of Stirling-PDFs existing ones
|
||||||
|
|
||||||
### Extra notes
|
### Extra notes
|
||||||
|
|
||||||
- Endpoints. Currently, the endpoints ENDPOINTS_TO_REMOVE and GROUPS_TO_REMOVE can include comma separate lists of endpoints and groups to disable as example ENDPOINTS_TO_REMOVE=img-to-pdf,remove-pages would disable both image-to-pdf and remove pages, GROUPS_TO_REMOVE=LibreOffice Would disable all things that use LibreOffice. You can see a list of all endpoints and groups [here](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/Endpoint-groups.md)
|
- Endpoints. Currently, the endpoints ENDPOINTS_TO_REMOVE and GROUPS_TO_REMOVE can include comma separate lists of endpoints and groups to disable as example ENDPOINTS_TO_REMOVE=img-to-pdf,remove-pages would disable both image-to-pdf and remove pages, GROUPS_TO_REMOVE=LibreOffice Would disable all things that use LibreOffice. You can see a list of all endpoints and groups [here](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/Endpoint-groups.md)
|
||||||
- customStaticFilePath. Customise static files such as the app logo by placing files in the /customFiles/static/ directory. An example of customising app logo is placing a /customFiles/static/favicon.svg to override current SVG. This can be used to change any images/icons/css/fonts/js etc in Stirling-PDF
|
- customStaticFilePath. Customise static files such as the app logo by placing files in the /customFiles/static/ directory. An example of customising app logo is placing a /customFiles/static/favicon.svg to override current SVG. This can be used to change any images/icons/css/fonts/js etc in Stirling-PDF
|
||||||
|
|
||||||
### Environment only parameters
|
### Environment only parameters
|
||||||
|
|
||||||
- ``SYSTEM_ROOTURIPATH`` ie set to ``/pdf-app`` to Set the application's root URI to ``localhost:8080/pdf-app``
|
- ``SYSTEM_ROOTURIPATH`` ie set to ``/pdf-app`` to Set the application's root URI to ``localhost:8080/pdf-app``
|
||||||
- ``SYSTEM_CONNECTIONTIMEOUTMINUTES`` to set custom connection timeout values
|
- ``SYSTEM_CONNECTIONTIMEOUTMINUTES`` to set custom connection timeout values
|
||||||
- ``DOCKER_ENABLE_SECURITY`` to tell docker to download security jar (required as true for auth login)
|
- ``DOCKER_ENABLE_SECURITY`` to tell docker to download security jar (required as true for auth login)
|
||||||
|
- ``INSTALL_BOOK_AND_ADVANCED_HTML_OPS`` to download calibre onto stirling-pdf enabling pdf to/from book and advanced html conversion
|
||||||
|
- ``LANGS`` to define custom font libraries to install for use for document conversions
|
||||||
|
|
||||||
## 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/Stirling-Tools/Stirling-PDF/) or navigate to /swagger-ui/index.html of your stirling-pdf instance for your versions documentation (Or by following the API button in your settings of Stirling-PDF)
|
[here](https://app.swaggerhub.com/apis-docs/Stirling-Tools/Stirling-PDF/) or navigate to /swagger-ui/index.html of your stirling-pdf instance for your versions documentation (Or by following the API button in your settings of Stirling-PDF)
|
||||||
|
|
||||||
|
|
||||||
## Login authentication
|
## Login authentication
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
### Prerequisites:
|
### Prerequisites:
|
||||||
|
|
||||||
- User must have the folder ./configs volumed within docker so that it is retained during updates.
|
- User must have the folder ./configs volumed within docker so that it is retained during updates.
|
||||||
- Docker uses must download the security jar version by setting ``DOCKER_ENABLE_SECURITY`` to ``true`` in environment variables.
|
- Docker users 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``
|
- 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).
|
- 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).
|
||||||
|
|
||||||
@@ -252,20 +300,22 @@ To add new users go to the bottom of Account settings and hit 'Admin Settings',
|
|||||||
|
|
||||||
For API usage you must provide a header with 'X-API-Key' and the associated API key for that user.
|
For API usage you must provide a header with 'X-API-Key' and the associated API key for that user.
|
||||||
|
|
||||||
|
|
||||||
## FAQ
|
## FAQ
|
||||||
|
|
||||||
### Q1: What are your planned features?
|
### Q1: What are your planned features?
|
||||||
|
|
||||||
- Progress bar/Tracking
|
- Progress bar/Tracking
|
||||||
- Full custom logic pipelines to combine multiple operations together.
|
- Full custom logic pipelines to combine multiple operations together.
|
||||||
- Folder support with auto scanning to perform operations on
|
- Folder support with auto scanning to perform operations on
|
||||||
- Redact text (Via UI not just automated way)
|
- Redact text (Via UI not just automated way)
|
||||||
- Add Forms
|
- Add Forms
|
||||||
- Multi page layout (Stich PDF pages together) support x rows y columns and custom page sizing
|
- Multi page layout (Stich PDF pages together) support x rows y columns and custom page sizing
|
||||||
- Fill forms mannual and automatic
|
- Fill forms manually or automatically
|
||||||
|
|
||||||
### Q2: Why is my application downloading .htm files?
|
### Q2: Why is my application downloading .htm files?
|
||||||
|
|
||||||
This is an issue caused commonly by your NGINX configuration. The default file upload size for NGINX is 1MB, you need to add the following in your Nginx sites-available file. ``client_max_body_size SIZE;`` Where "SIZE" is 50M for example for 50MB files.
|
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
|
### 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;``
|
NGINX has timeout values by default so if you are running Stirling-PDF behind NGINX you may need to set a timeout value such as adding the config ``proxy_read_timeout 3600;``
|
||||||
|
|||||||
@@ -1,64 +1,52 @@
|
|||||||
|Technology | Ultra-Lite | Lite | Full |
|
| Technology | Ultra-Lite | Full |
|
||||||
|----------------|:----------:|:----:|:----:|
|
|----------------|:----------:|:----:|
|
||||||
| Java | ✔️ | ✔️ | ✔️ |
|
| Java | ✔️ | ✔️ |
|
||||||
| JavaScript | ✔️ | ✔️ | ✔️ |
|
| JavaScript | ✔️ | ✔️ |
|
||||||
| Libre | | ✔️ | ✔️ |
|
| Libre | | ✔️ |
|
||||||
| Python | | | ✔️ |
|
| Python | | ✔️ |
|
||||||
| OpenCV | | | ✔️ |
|
| OpenCV | | ✔️ |
|
||||||
| OCRmyPDF | | | ✔️ |
|
| OCRmyPDF | | ✔️ |
|
||||||
|
|
||||||
|
Operation | Ultra-Lite | Full
|
||||||
|
-------------------------|------------|-----
|
||||||
|
add-page-numbers | ✔️ | ✔️
|
||||||
|
add-password | ✔️ | ✔️
|
||||||
Operation | Ultra-Lite | Lite | Full
|
add-image | ✔️ | ✔️
|
||||||
--------------------|------------|------|-----
|
add-watermark | ✔️ | ✔️
|
||||||
add-page-numbers | ✔️ | ✔️ | ✔️
|
adjust-contrast | ✔️ | ✔️
|
||||||
add-password | ✔️ | ✔️ | ✔️
|
auto-split-pdf | ✔️ | ✔️
|
||||||
add-image | ✔️ | ✔️ | ✔️
|
auto-redact | ✔️ | ✔️
|
||||||
add-watermark | ✔️ | ✔️ | ✔️
|
auto-rename | ✔️ | ✔️
|
||||||
adjust-contrast | ✔️ | ✔️ | ✔️
|
cert-sign | ✔️ | ✔️
|
||||||
auto-split-pdf | ✔️ | ✔️ | ✔️
|
crop | ✔️ | ✔️
|
||||||
auto-redact | ✔️ | ✔️ | ✔️
|
change-metadata | ✔️ | ✔️
|
||||||
auto-rename | ✔️ | ✔️ | ✔️
|
change-permissions | ✔️ | ✔️
|
||||||
cert-sign | ✔️ | ✔️ | ✔️
|
compare | ✔️ | ✔️
|
||||||
crop | ✔️ | ✔️ | ✔️
|
extract-page | ✔️ | ✔️
|
||||||
change-metadata | ✔️ | ✔️ | ✔️
|
extract-images | ✔️ | ✔️
|
||||||
change-permissions | ✔️ | ✔️ | ✔️
|
flatten | ✔️ | ✔️
|
||||||
compare | ✔️ | ✔️ | ✔️
|
get-info-on-pdf | ✔️ | ✔️
|
||||||
extract-page | ✔️ | ✔️ | ✔️
|
img-to-pdf | ✔️ | ✔️
|
||||||
extract-images | ✔️ | ✔️ | ✔️
|
markdown-to-pdf | ✔️ | ✔️
|
||||||
flatten | ✔️ | ✔️ | ✔️
|
merge-pdfs | ✔️ | ✔️
|
||||||
get-info-on-pdf | ✔️ | ✔️ | ✔️
|
multi-page-layout | ✔️ | ✔️
|
||||||
img-to-pdf | ✔️ | ✔️ | ✔️
|
overlay-pdf | ✔️ | ✔️
|
||||||
markdown-to-pdf | ✔️ | ✔️ | ✔️
|
pdf-organizer | ✔️ | ✔️
|
||||||
merge-pdfs | ✔️ | ✔️ | ✔️
|
pdf-to-csv | ✔️ | ✔️
|
||||||
multi-page-layout | ✔️ | ✔️ | ✔️
|
pdf-to-img | ✔️ | ✔️
|
||||||
overlay-pdf | ✔️ | ✔️ | ✔️
|
pdf-to-single-page | ✔️ | ✔️
|
||||||
pdf-organizer | ✔️ | ✔️ | ✔️
|
remove-pages | ✔️ | ✔️
|
||||||
pdf-to-csv | ✔️ | ✔️ | ✔️
|
remove-password | ✔️ | ✔️
|
||||||
pdf-to-img | ✔️ | ✔️ | ✔️
|
rotate-pdf | ✔️ | ✔️
|
||||||
pdf-to-single-page | ✔️ | ✔️ | ✔️
|
sanitize-pdf | ✔️ | ✔️
|
||||||
remove-pages | ✔️ | ✔️ | ✔️
|
scale-pages | ✔️ | ✔️
|
||||||
remove-password | ✔️ | ✔️ | ✔️
|
sign | ✔️ | ✔️
|
||||||
rotate-pdf | ✔️ | ✔️ | ✔️
|
show-javascript | ✔️ | ✔️
|
||||||
sanitize-pdf | ✔️ | ✔️ | ✔️
|
split-by-size-or-count | ✔️ | ✔️
|
||||||
scale-pages | ✔️ | ✔️ | ✔️
|
split-pdf-by-sections | ✔️ | ✔️
|
||||||
sign | ✔️ | ✔️ | ✔️
|
split-pdfs | ✔️ | ✔️
|
||||||
show-javascript | ✔️ | ✔️ | ✔️
|
compress-pdf | | ✔️
|
||||||
split-by-size-or-count | ✔️ | ✔️ | ✔️
|
extract-image-scans | | ✔️
|
||||||
split-pdf-by-sections | ✔️ | ✔️ | ✔️
|
ocr-pdf | | ✔️
|
||||||
split-pdfs | ✔️ | ✔️ | ✔️
|
pdf-to-pdfa | | ✔️
|
||||||
file-to-pdf | | ✔️ | ✔️
|
remove-blanks | | ✔️
|
||||||
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 | | | ✔️
|
|
||||||
|
|||||||
69
build.gradle
69
build.gradle
@@ -1,18 +1,20 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id 'java'
|
id 'java'
|
||||||
id 'org.springframework.boot' version '3.2.1'
|
id 'org.springframework.boot' version '3.2.4'
|
||||||
id 'io.spring.dependency-management' version '1.1.3'
|
id 'io.spring.dependency-management' version '1.1.3'
|
||||||
id 'org.springdoc.openapi-gradle-plugin' version '1.8.0'
|
id 'org.springdoc.openapi-gradle-plugin' version '1.8.0'
|
||||||
id "io.swagger.swaggerhub" version "1.3.2"
|
id "io.swagger.swaggerhub" version "1.3.2"
|
||||||
id 'edu.sc.seis.launch4j' version '3.0.5'
|
id 'edu.sc.seis.launch4j' version '3.0.5'
|
||||||
id 'com.diffplug.spotless' version '6.23.3'
|
id 'com.diffplug.spotless' version '6.25.0'
|
||||||
id 'com.github.jk1.dependency-license-report' version '2.5'
|
id 'com.github.jk1.dependency-license-report' version '2.6'
|
||||||
}
|
}
|
||||||
|
|
||||||
import com.github.jk1.license.render.*
|
import com.github.jk1.license.render.*
|
||||||
|
|
||||||
group = 'stirling.software'
|
group = 'stirling.software'
|
||||||
version = '0.20.0'
|
version = '0.24.0'
|
||||||
|
|
||||||
|
//17 is lowest but we support and recommend 21
|
||||||
sourceCompatibility = '17'
|
sourceCompatibility = '17'
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
@@ -20,7 +22,6 @@ repositories {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
licenseReport {
|
licenseReport {
|
||||||
renderers = [new JsonReportRenderer()]
|
renderers = [new JsonReportRenderer()]
|
||||||
}
|
}
|
||||||
@@ -48,7 +49,6 @@ openApi {
|
|||||||
outputFileName = "SwaggerDoc.json"
|
outputFileName = "SwaggerDoc.json"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
launch4j {
|
launch4j {
|
||||||
icon = "${projectDir}/src/main/resources/static/favicon.ico"
|
icon = "${projectDir}/src/main/resources/static/favicon.ico"
|
||||||
|
|
||||||
@@ -56,8 +56,8 @@ launch4j {
|
|||||||
headerType="console"
|
headerType="console"
|
||||||
jarTask = tasks.bootJar
|
jarTask = tasks.bootJar
|
||||||
|
|
||||||
errTitle="Encountered error, Do you have Java 17?"
|
errTitle="Encountered error, Do you have Java 21?"
|
||||||
downloadUrl="https://download.oracle.com/java/17/latest/jdk-17_windows-x64_bin.exe"
|
downloadUrl="https://download.oracle.com/java/21/latest/jdk-21_windows-x64_bin.exe"
|
||||||
variables=["BROWSER_OPEN=true", "ENDPOINTS_GROUPS_TO_REMOVE=CLI"]
|
variables=["BROWSER_OPEN=true", "ENDPOINTS_GROUPS_TO_REMOVE=CLI"]
|
||||||
jreMinVersion="17"
|
jreMinVersion="17"
|
||||||
|
|
||||||
@@ -66,8 +66,8 @@ launch4j {
|
|||||||
|
|
||||||
messagesStartupError="An error occurred while starting Stirling-PDF"
|
messagesStartupError="An error occurred while starting Stirling-PDF"
|
||||||
//messagesJreNotFoundError="This application requires a Java Runtime Environment, Please download Java 17."
|
//messagesJreNotFoundError="This application requires a Java Runtime Environment, Please download Java 17."
|
||||||
messagesJreVersionError="You are running the wrong version of Java, Please download Java 17."
|
messagesJreVersionError="You are running the wrong version of Java, Please download Java 21."
|
||||||
messagesLauncherError="Java is corrupted. Please uninstall and then install Java 17."
|
messagesLauncherError="Java is corrupted. Please uninstall and then install Java 21."
|
||||||
messagesInstanceAlreadyExists="Stirling-PDF is already running."
|
messagesInstanceAlreadyExists="Stirling-PDF is already running."
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,24 +87,27 @@ spotless {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
//security updates
|
//security updates
|
||||||
implementation 'ch.qos.logback:logback-classic:1.4.14'
|
implementation 'ch.qos.logback:logback-classic:1.5.3'
|
||||||
implementation 'ch.qos.logback:logback-core:1.4.14'
|
implementation 'ch.qos.logback:logback-core:1.5.3'
|
||||||
implementation 'org.springframework:spring-webmvc:6.1.2'
|
implementation 'org.springframework:spring-webmvc:6.1.5'
|
||||||
|
|
||||||
|
implementation("io.github.pixee:java-security-toolkit:1.1.3")
|
||||||
|
|
||||||
implementation 'org.yaml:snakeyaml:2.2'
|
implementation 'org.yaml:snakeyaml:2.2'
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-web:3.2.1'
|
implementation 'org.springframework.boot:spring-boot-starter-web:3.2.4'
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf:3.2.1'
|
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf:3.2.4'
|
||||||
|
|
||||||
if (System.getenv('DOCKER_ENABLE_SECURITY') != 'false') {
|
if (System.getenv('DOCKER_ENABLE_SECURITY') != 'false') {
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-security:3.2.1'
|
implementation 'org.springframework.boot:spring-boot-starter-security:3.2.4'
|
||||||
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5:3.1.2.RELEASE'
|
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5:3.1.2.RELEASE'
|
||||||
implementation "org.springframework.boot:spring-boot-starter-data-jpa:3.2.1"
|
implementation "org.springframework.boot:spring-boot-starter-data-jpa:3.2.4"
|
||||||
|
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client:3.2.4'
|
||||||
|
|
||||||
//2.2.x requires rebuild of DB file.. need migration path
|
//2.2.x requires rebuild of DB file.. need migration path
|
||||||
implementation "com.h2database:h2:2.1.214"
|
implementation "com.h2database:h2:2.1.214"
|
||||||
}
|
}
|
||||||
|
|
||||||
testImplementation 'org.springframework.boot:spring-boot-starter-test:3.2.1'
|
testImplementation 'org.springframework.boot:spring-boot-starter-test:3.2.4'
|
||||||
|
|
||||||
// Batik
|
// Batik
|
||||||
implementation 'org.apache.xmlgraphics:batik-all:1.17'
|
implementation 'org.apache.xmlgraphics:batik-all:1.17'
|
||||||
@@ -136,29 +139,31 @@ dependencies {
|
|||||||
implementation ('com.opencsv:opencsv:5.9') {
|
implementation ('com.opencsv:opencsv:5.9') {
|
||||||
exclude group: 'commons-logging', module: 'commons-logging'
|
exclude group: 'commons-logging', module: 'commons-logging'
|
||||||
}
|
}
|
||||||
|
|
||||||
implementation ('org.apache.pdfbox:pdfbox:3.0.1'){
|
implementation ('org.apache.pdfbox:pdfbox:3.0.2'){
|
||||||
exclude group: 'commons-logging', module: 'commons-logging'
|
exclude group: 'commons-logging', module: 'commons-logging'
|
||||||
}
|
}
|
||||||
|
|
||||||
implementation ('org.apache.pdfbox:xmpbox:3.0.1'){
|
implementation ('org.apache.pdfbox:xmpbox:3.0.2'){
|
||||||
exclude group: 'commons-logging', module: 'commons-logging'
|
exclude group: 'commons-logging', module: 'commons-logging'
|
||||||
}
|
}
|
||||||
|
|
||||||
implementation 'org.bouncycastle:bcprov-jdk18on:1.77'
|
implementation 'org.bouncycastle:bcprov-jdk18on:1.77'
|
||||||
implementation 'org.bouncycastle:bcpkix-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:3.2.4'
|
||||||
implementation 'io.micrometer:micrometer-core'
|
implementation 'io.micrometer:micrometer-core:1.12.4'
|
||||||
implementation group: 'com.google.zxing', name: 'core', version: '3.5.2'
|
implementation group: 'com.google.zxing', name: 'core', version: '3.5.3'
|
||||||
// https://mvnrepository.com/artifact/org.commonmark/commonmark
|
// https://mvnrepository.com/artifact/org.commonmark/commonmark
|
||||||
implementation 'org.commonmark:commonmark:0.21.0'
|
implementation 'org.commonmark:commonmark:0.22.0'
|
||||||
implementation 'org.commonmark:commonmark-ext-gfm-tables:0.21.0'
|
implementation 'org.commonmark:commonmark-ext-gfm-tables:0.22.0'
|
||||||
// https://mvnrepository.com/artifact/com.github.vladimir-bukhtoyarov/bucket4j-core
|
// https://mvnrepository.com/artifact/com.github.vladimir-bukhtoyarov/bucket4j-core
|
||||||
implementation 'com.github.vladimir-bukhtoyarov:bucket4j-core:7.6.0'
|
implementation 'com.github.vladimir-bukhtoyarov:bucket4j-core:7.6.0'
|
||||||
|
|
||||||
developmentOnly("org.springframework.boot:spring-boot-devtools")
|
implementation 'com.fathzer:javaluator:3.0.3'
|
||||||
compileOnly 'org.projectlombok:lombok:1.18.30'
|
|
||||||
annotationProcessor 'org.projectlombok:lombok:1.18.28'
|
developmentOnly("org.springframework.boot:spring-boot-devtools:3.2.4")
|
||||||
|
compileOnly 'org.projectlombok:lombok:1.18.32'
|
||||||
|
annotationProcessor 'org.projectlombok:lombok:1.18.32'
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType(JavaCompile) {
|
tasks.withType(JavaCompile) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
apiVersion: v2
|
apiVersion: v2
|
||||||
appVersion: 0.14.2
|
appVersion: 0.24.0
|
||||||
description: locally hosted web application that allows you to perform various operations on PDF files
|
description: locally hosted web application that allows you to perform various operations
|
||||||
|
on PDF files
|
||||||
home: https://github.com/Stirling-Tools/Stirling-PDF
|
home: https://github.com/Stirling-Tools/Stirling-PDF
|
||||||
keywords:
|
keywords:
|
||||||
- stirling-pdf
|
- stirling-pdf
|
||||||
|
|||||||
@@ -43,6 +43,6 @@ spec:
|
|||||||
name: http
|
name: http
|
||||||
{{- end }}
|
{{- end }}
|
||||||
protocol: TCP
|
protocol: TCP
|
||||||
|
|
||||||
selector:
|
selector:
|
||||||
{{- include "stirlingpdf.selectorLabels" . | nindent 4 }}
|
{{- include "stirlingpdf.selectorLabels" . | nindent 4 }}
|
||||||
|
|||||||
@@ -16,11 +16,11 @@ commonLabels: {}
|
|||||||
# team_name: dev
|
# team_name: dev
|
||||||
|
|
||||||
envs: []
|
envs: []
|
||||||
# - name: PP_HOME_NAME
|
# - name: UI_APP_NAME
|
||||||
# value: "Stirling PDF"
|
# value: "Stirling PDF"
|
||||||
# - name: APP_HOME_DESCRIPTION
|
# - name: UI_HOME_DESCRIPTION
|
||||||
# value: "Your locally hosted one-stop-shop for all your PDF needs."
|
# value: "Your locally hosted one-stop-shop for all your PDF needs."
|
||||||
# - name: APP_NAVBAR_NAME
|
# - name: UI_APP_NAVBAR_NAME
|
||||||
# value: "Stirling PDF"
|
# value: "Stirling PDF"
|
||||||
# - name: ALLOW_GOOGLE_VISIBILITY
|
# - name: ALLOW_GOOGLE_VISIBILITY
|
||||||
# value: "true"
|
# value: "true"
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 8.7 KiB |
@@ -1,310 +1,110 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
|
||||||
<svg
|
<svg
|
||||||
width="99.537987mm"
|
|
||||||
height="95.209366mm"
|
|
||||||
viewBox="0 0 99.537987 95.209366"
|
|
||||||
version="1.1"
|
version="1.1"
|
||||||
id="svg745"
|
id="Layer_1"
|
||||||
|
x="0px"
|
||||||
|
y="0px"
|
||||||
|
viewBox="0 0 512 512"
|
||||||
|
style="enable-background:new 0 0 512 512;"
|
||||||
xml:space="preserve"
|
xml:space="preserve"
|
||||||
inkscape:version="1.2.1 (9c6d41e4, 2022-07-14)"
|
sodipodi:docname="favicon.svg"
|
||||||
sodipodi:docname="stirling.svg"
|
inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
|
||||||
inkscape:export-filename="stirling.png"
|
inkscape:export-filename="favicon.png"
|
||||||
inkscape:export-xdpi="80"
|
inkscape:export-xdpi="96"
|
||||||
inkscape:export-ydpi="80"
|
inkscape:export-ydpi="96"
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
|
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||||
id="namedview747"
|
id="defs173">
|
||||||
pagecolor="#ffffff"
|
|
||||||
bordercolor="#666666"
|
|
||||||
borderopacity="1.0"
|
<linearGradient
|
||||||
inkscape:showpageshadow="2"
|
id="XMLID_5_"
|
||||||
inkscape:pageopacity="0.0"
|
gradientUnits="userSpaceOnUse"
|
||||||
inkscape:pagecheckerboard="0"
|
x1="304.496"
|
||||||
inkscape:deskcolor="#d1d1d1"
|
y1="422.9102"
|
||||||
inkscape:document-units="mm"
|
x2="316.036"
|
||||||
showgrid="false"
|
y2="326.2626">
|
||||||
inkscape:zoom="0.914906"
|
<stop
|
||||||
inkscape:cx="184.17193"
|
offset="0"
|
||||||
inkscape:cy="509.88845"
|
style="stop-color:#DCF1F3"
|
||||||
inkscape:window-width="2293"
|
id="stop156" />
|
||||||
inkscape:window-height="1387"
|
<stop
|
||||||
inkscape:window-x="122"
|
offset="1"
|
||||||
inkscape:window-y="25"
|
style="stop-color:#C2C2C9"
|
||||||
inkscape:window-maximized="0"
|
id="stop158" />
|
||||||
inkscape:current-layer="svg745" /><defs
|
</linearGradient>
|
||||||
id="defs742"><linearGradient
|
|
||||||
inkscape:collect="always"
|
</defs><sodipodi:namedview
|
||||||
id="linearGradient72198"><stop
|
id="namedview171"
|
||||||
style="stop-color:#ccd6d7;stop-opacity:1;"
|
pagecolor="#ffffff"
|
||||||
offset="0"
|
bordercolor="#000000"
|
||||||
id="stop72194" /><stop
|
borderopacity="0.25"
|
||||||
style="stop-color:#0f3a3f;stop-opacity:1;"
|
inkscape:showpageshadow="2"
|
||||||
offset="1"
|
inkscape:pageopacity="0.0"
|
||||||
id="stop72196" /></linearGradient><linearGradient
|
inkscape:pagecheckerboard="0"
|
||||||
inkscape:collect="always"
|
inkscape:deskcolor="#d1d1d1"
|
||||||
id="linearGradient71928"><stop
|
showgrid="false"
|
||||||
style="stop-color:#4b0005;stop-opacity:1;"
|
inkscape:zoom="1.4142136"
|
||||||
offset="0"
|
inkscape:cx="219.91021"
|
||||||
id="stop71924" /><stop
|
inkscape:cy="232.63813"
|
||||||
style="stop-color:#8f000c;stop-opacity:1;"
|
inkscape:window-width="3840"
|
||||||
offset="1"
|
inkscape:window-height="2054"
|
||||||
id="stop71926" /></linearGradient><linearGradient
|
inkscape:window-x="2869"
|
||||||
inkscape:collect="always"
|
inkscape:window-y="-11"
|
||||||
id="linearGradient71920"><stop
|
inkscape:window-maximized="1"
|
||||||
style="stop-color:#4b0005;stop-opacity:1;"
|
inkscape:current-layer="XMLID_4_" />
|
||||||
offset="0"
|
<style
|
||||||
id="stop71916" /><stop
|
type="text/css"
|
||||||
style="stop-color:#8f000c;stop-opacity:1;"
|
id="style150">
|
||||||
offset="1"
|
.st0{fill:#FFFFFF;}
|
||||||
id="stop71918" /></linearGradient><linearGradient
|
.st1{fill:#C02223;}
|
||||||
inkscape:collect="always"
|
.st2{fill:#882425;}
|
||||||
id="linearGradient69598"><stop
|
.st3{fill:url(#XMLID_5_);}
|
||||||
style="stop-color:#6a0007;stop-opacity:1;"
|
.st4{fill:url(#XMLID_7_);}
|
||||||
offset="0"
|
</style>
|
||||||
id="stop69594" /><stop
|
|
||||||
style="stop-color:#b8000f;stop-opacity:1;"
|
<g
|
||||||
offset="1"
|
id="XMLID_4_">
|
||||||
id="stop69596" /></linearGradient><linearGradient
|
<path
|
||||||
inkscape:collect="always"
|
id="XMLID_131_"
|
||||||
id="linearGradient46361"><stop
|
class="st1"
|
||||||
style="stop-color:#f7f6f8;stop-opacity:1;"
|
d="M 347.01402,14.355825 98.978019,69.02261 C 73.825483,74.547445 55.942464,96.792175 55.942464,122.52628 v 315.06096 c 0,22.39012 16.719895,41.14548 38.819234,43.76251 L 224.8861,498.36042 339.48636,384.26465 455.76603,265.15425 453.73057,84.870162 C 453.43979,62.916214 433.08513,46.632491 411.71274,51.284984 l -28.78729,6.251786 0.14539,-13.666697 C 383.36162,24.678542 365.62399,10.284894 347.01402,14.355825 Z"
|
||||||
offset="0"
|
sodipodi:nodetypes="ccssccccccccc"
|
||||||
id="stop46359" /><stop
|
style="stroke-width:1.45391" /><path
|
||||||
style="stop-color:#b7b7b5;stop-opacity:1;"
|
id="XMLID_117_"
|
||||||
offset="1"
|
class="st2"
|
||||||
id="stop46357" /></linearGradient><linearGradient
|
d="m 383.21622,57.53677 v 285.8375 L 456.05681,265.00885 454.02135,78.763767 C 453.87595,59.863016 436.28372,45.905539 417.81914,49.97647 Z"
|
||||||
inkscape:collect="always"
|
style="stroke-width:1.45391" /><polygon
|
||||||
id="linearGradient40554"><stop
|
id="XMLID_18_"
|
||||||
style="stop-color:#f4f2f4;stop-opacity:1;"
|
class="st3"
|
||||||
offset="0"
|
points="234.7,422.6 368.5,387.7 393.5,262.2 "
|
||||||
id="stop40550" /><stop
|
style="fill:url(#XMLID_5_)"
|
||||||
style="stop-color:#57767b;stop-opacity:1;"
|
transform="matrix(1.4556308,0,0,1.4548265,-116.73161,-116.45231)" />
|
||||||
offset="1"
|
<linearGradient
|
||||||
id="stop40552" /></linearGradient><linearGradient
|
id="XMLID_7_"
|
||||||
inkscape:collect="always"
|
gradientUnits="userSpaceOnUse"
|
||||||
id="linearGradient39095"><stop
|
x1="223.0838"
|
||||||
style="stop-color:#285459;stop-opacity:1;"
|
y1="372.7559"
|
||||||
offset="0"
|
x2="241.4174"
|
||||||
id="stop39093" /><stop
|
y2="114.557"
|
||||||
style="stop-color:#a6b6af;stop-opacity:1;"
|
gradientTransform="matrix(1.4539039,0,0,1.4539039,-116.19976,-116.20474)">
|
||||||
offset="1"
|
<stop
|
||||||
id="stop39091" /></linearGradient><linearGradient
|
offset="0"
|
||||||
inkscape:collect="always"
|
style="stop-color:#DCF1F3"
|
||||||
id="linearGradient36672"><stop
|
id="stop163" />
|
||||||
style="stop-color:#da453f;stop-opacity:1;"
|
<stop
|
||||||
offset="0"
|
offset="1"
|
||||||
id="stop36668" /><stop
|
style="stop-color:#C2C2C9"
|
||||||
style="stop-color:#a60008;stop-opacity:1;"
|
id="stop165" />
|
||||||
offset="1"
|
</linearGradient>
|
||||||
id="stop36670" /></linearGradient><linearGradient
|
<path
|
||||||
inkscape:collect="always"
|
id="XMLID_6_"
|
||||||
id="linearGradient19524"><stop
|
class="st4"
|
||||||
style="stop-color:#3a4b4f;stop-opacity:1;"
|
d="m 282.89686,214.84917 c 0,0 -22.24473,-28.93269 -38.67384,-36.78377 -10.46811,-4.94327 -26.02489,-6.83335 -38.23768,-0.72695 -18.02841,9.0142 -19.91848,34.31213 -3.34397,44.34406 3.92553,2.47165 9.15959,4.50711 15.99294,6.10641 36.63838,8.43264 97.12077,25.87949 89.70587,96.10304 0,0 -4.21633,65.86185 -73.56753,73.42215 -12.2128,1.30851 -24.57098,0.43617 -36.493,-2.32625 -16.42911,-3.63476 -45.50719,-11.04967 -59.75545,-19.91849 l -2.61703,-75.16682 h 6.97875 c 0,0 13.81208,33.43978 53.06749,49.57812 7.26952,2.90781 15.26599,4.07093 22.97168,2.90781 9.74116,-1.45391 21.22699,-6.68796 25.87949,-22.53551 0,0 7.85108,-23.11707 -32.85823,-35.76604 -32.56744,-10.17733 -63.24481,-20.64543 -75.89378,-54.95757 -5.961,-16.28371 -6.97874,-34.31212 -2.90781,-51.61358 5.37944,-22.53551 20.79082,-54.23062 64.40794,-67.89732 0,0 57.28381,-15.55677 96.53922,5.52484 l -1.74468,89.70587 z"
|
||||||
offset="0"
|
style="fill:url(#XMLID_7_);stroke-width:1.45391" />
|
||||||
id="stop19522" /><stop
|
</g>
|
||||||
style="stop-color:#617979;stop-opacity:0.97461927;"
|
</svg>
|
||||||
offset="1"
|
|
||||||
id="stop19520" /></linearGradient><linearGradient
|
|
||||||
inkscape:collect="always"
|
|
||||||
id="linearGradient17996"><stop
|
|
||||||
style="stop-color:#401016;stop-opacity:1;"
|
|
||||||
offset="0"
|
|
||||||
id="stop17994" /><stop
|
|
||||||
style="stop-color:#761f19;stop-opacity:1;"
|
|
||||||
offset="1"
|
|
||||||
id="stop17992" /></linearGradient><linearGradient
|
|
||||||
inkscape:collect="always"
|
|
||||||
id="linearGradient15569"><stop
|
|
||||||
style="stop-color:#8ea8ad;stop-opacity:1;"
|
|
||||||
offset="0"
|
|
||||||
id="stop15565" /><stop
|
|
||||||
style="stop-color:#e9e7eb;stop-opacity:1;"
|
|
||||||
offset="1"
|
|
||||||
id="stop15567" /></linearGradient><linearGradient
|
|
||||||
inkscape:collect="always"
|
|
||||||
id="linearGradient15557"><stop
|
|
||||||
style="stop-color:#9b0e11;stop-opacity:1;"
|
|
||||||
offset="0"
|
|
||||||
id="stop15553" /><stop
|
|
||||||
style="stop-color:#370707;stop-opacity:1;"
|
|
||||||
offset="1"
|
|
||||||
id="stop15555" /></linearGradient><linearGradient
|
|
||||||
inkscape:collect="always"
|
|
||||||
xlink:href="#linearGradient15557"
|
|
||||||
id="linearGradient15559"
|
|
||||||
x1="120.06672"
|
|
||||||
y1="63.25761"
|
|
||||||
x2="135.16347"
|
|
||||||
y2="78.078682"
|
|
||||||
gradientUnits="userSpaceOnUse" /><linearGradient
|
|
||||||
inkscape:collect="always"
|
|
||||||
xlink:href="#linearGradient15569"
|
|
||||||
id="linearGradient15571"
|
|
||||||
x1="127.97037"
|
|
||||||
y1="101.66144"
|
|
||||||
x2="133.88971"
|
|
||||||
y2="104.77026"
|
|
||||||
gradientUnits="userSpaceOnUse" /><linearGradient
|
|
||||||
inkscape:collect="always"
|
|
||||||
xlink:href="#linearGradient17996"
|
|
||||||
id="linearGradient17998"
|
|
||||||
x1="117.9284"
|
|
||||||
y1="86.055084"
|
|
||||||
x2="130.67392"
|
|
||||||
y2="76.945541"
|
|
||||||
gradientUnits="userSpaceOnUse" /><linearGradient
|
|
||||||
inkscape:collect="always"
|
|
||||||
xlink:href="#linearGradient19524"
|
|
||||||
id="linearGradient19528"
|
|
||||||
x1="130.98172"
|
|
||||||
y1="82.402977"
|
|
||||||
x2="135.72115"
|
|
||||||
y2="86.45166"
|
|
||||||
gradientUnits="userSpaceOnUse" /><linearGradient
|
|
||||||
inkscape:collect="always"
|
|
||||||
xlink:href="#linearGradient36672"
|
|
||||||
id="linearGradient36674"
|
|
||||||
x1="63.797714"
|
|
||||||
y1="74.81752"
|
|
||||||
x2="96.636673"
|
|
||||||
y2="120.29293"
|
|
||||||
gradientUnits="userSpaceOnUse" /><linearGradient
|
|
||||||
inkscape:collect="always"
|
|
||||||
xlink:href="#linearGradient39095"
|
|
||||||
id="linearGradient39097"
|
|
||||||
x1="120.54506"
|
|
||||||
y1="124.76902"
|
|
||||||
x2="128.04152"
|
|
||||||
y2="126.0704"
|
|
||||||
gradientUnits="userSpaceOnUse" /><linearGradient
|
|
||||||
inkscape:collect="always"
|
|
||||||
xlink:href="#linearGradient40554"
|
|
||||||
id="linearGradient40556"
|
|
||||||
x1="113.84585"
|
|
||||||
y1="114.04285"
|
|
||||||
x2="119.65858"
|
|
||||||
y2="128.50244"
|
|
||||||
gradientUnits="userSpaceOnUse" /><linearGradient
|
|
||||||
inkscape:collect="always"
|
|
||||||
xlink:href="#linearGradient46361"
|
|
||||||
id="linearGradient46363"
|
|
||||||
x1="73.993439"
|
|
||||||
y1="114.13906"
|
|
||||||
x2="94.845322"
|
|
||||||
y2="71.247383"
|
|
||||||
gradientUnits="userSpaceOnUse" /><linearGradient
|
|
||||||
inkscape:collect="always"
|
|
||||||
xlink:href="#linearGradient69598"
|
|
||||||
id="linearGradient69600"
|
|
||||||
x1="95.854446"
|
|
||||||
y1="114.66749"
|
|
||||||
x2="103.77842"
|
|
||||||
y2="120.1887"
|
|
||||||
gradientUnits="userSpaceOnUse" /><linearGradient
|
|
||||||
inkscape:collect="always"
|
|
||||||
xlink:href="#linearGradient71920"
|
|
||||||
id="linearGradient71922"
|
|
||||||
x1="98.580376"
|
|
||||||
y1="87.186958"
|
|
||||||
x2="118.09738"
|
|
||||||
y2="101.19449"
|
|
||||||
gradientUnits="userSpaceOnUse" /><linearGradient
|
|
||||||
inkscape:collect="always"
|
|
||||||
xlink:href="#linearGradient71928"
|
|
||||||
id="linearGradient71930"
|
|
||||||
x1="78.278267"
|
|
||||||
y1="97.433273"
|
|
||||||
x2="92.313202"
|
|
||||||
y2="104.33479"
|
|
||||||
gradientUnits="userSpaceOnUse" /><linearGradient
|
|
||||||
inkscape:collect="always"
|
|
||||||
xlink:href="#linearGradient72198"
|
|
||||||
id="linearGradient72200"
|
|
||||||
x1="125.76636"
|
|
||||||
y1="138.46817"
|
|
||||||
x2="123.3327"
|
|
||||||
y2="126.03291"
|
|
||||||
gradientUnits="userSpaceOnUse" /></defs><g
|
|
||||||
inkscape:groupmode="layer"
|
|
||||||
id="layer4"
|
|
||||||
inkscape:label="background"
|
|
||||||
style="display:inline"
|
|
||||||
sodipodi:insensitive="true"
|
|
||||||
transform="translate(-51.420144,-44.470286)"><rect
|
|
||||||
style="display:inline;fill:#ccd6d7;fill-opacity:1;stroke:none;stroke-width:4.13755;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:3.2"
|
|
||||||
id="rect72067"
|
|
||||||
width="99.481979"
|
|
||||||
height="94.999512"
|
|
||||||
x="51.476147"
|
|
||||||
y="44.680138" /></g><g
|
|
||||||
inkscape:groupmode="layer"
|
|
||||||
id="layer5"
|
|
||||||
inkscape:label="shadow"
|
|
||||||
style="display:inline"
|
|
||||||
sodipodi:insensitive="true"
|
|
||||||
transform="translate(-51.420144,-44.470286)"><path
|
|
||||||
style="display:inline;fill:url(#linearGradient72200);fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
|
||||||
d="m 84.146049,134.73858 c 0,0 11.721038,2.48294 17.938661,2.91673 6.21763,0.43378 14.75251,0.59994 22.41237,-0.43379 8.01008,-1.081 13.19907,-2.22733 14.50043,-2.66112 1.30136,-0.43379 4.00784,-1.24297 4.15244,-2.25514 0.1446,-1.01217 -3.4703,-2.74733 -6.21763,-3.32571 -2.74732,-0.57838 -12.72444,-1.44596 -14.89337,-1.44596 -2.16894,0 -37.892901,7.20499 -37.892901,7.20499 z"
|
|
||||||
id="path72192"
|
|
||||||
sodipodi:nodetypes="cssssssc" /></g><g
|
|
||||||
inkscape:groupmode="layer"
|
|
||||||
id="layer2"
|
|
||||||
inkscape:label="Origami"
|
|
||||||
style="display:inline"
|
|
||||||
sodipodi:insensitive="true"
|
|
||||||
transform="translate(-51.420144,-44.470286)"><path
|
|
||||||
style="display:inline;fill:url(#linearGradient40556);fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
|
||||||
d="m 84.460552,134.86721 35.165798,-6.85679 16.15467,-42.7383 z"
|
|
||||||
id="path960"
|
|
||||||
sodipodi:nodetypes="cccc" /><path
|
|
||||||
style="fill:url(#linearGradient15571);fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
|
||||||
d="m 135.71493,85.428056 0.3984,45.049024 -9.66213,-20.46173 z"
|
|
||||||
id="path964"
|
|
||||||
sodipodi:nodetypes="cccc" /><path
|
|
||||||
style="display:inline;fill:url(#linearGradient39097);fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
|
||||||
d="m 119.60518,128.00293 16.5337,2.48693 -9.68769,-20.5512 z"
|
|
||||||
id="path966"
|
|
||||||
sodipodi:nodetypes="cccc" /><path
|
|
||||||
style="display:inline;fill:url(#linearGradient15559);fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
|
||||||
d="m 118.42209,57.022622 12.70423,-2.766809 -0.0785,25.087175 -12.43878,-13.376518 z"
|
|
||||||
id="path968"
|
|
||||||
sodipodi:nodetypes="ccccc" /><path
|
|
||||||
style="display:inline;fill:url(#linearGradient19528);fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
|
||||||
d="m 135.72114,85.386768 -4.84219,-6.459493 0.0305,11.126604 z"
|
|
||||||
id="path970"
|
|
||||||
sodipodi:nodetypes="cccc" /><path
|
|
||||||
style="display:inline;fill:url(#linearGradient17998);fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
|
||||||
d="m 119.10273,65.682415 11.96883,13.44935 -0.0899,10.819868 -11.88577,11.430427 z"
|
|
||||||
id="path972"
|
|
||||||
sodipodi:nodetypes="ccccc" /><path
|
|
||||||
style="display:inline;fill:url(#linearGradient36674);fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
|
||||||
d="M 62.145635,130.15618 62.043392,65.435258 c 0,0 0.179321,-2.778132 1.89516,-4.036097 1.874923,-1.374597 4.341768,-1.894096 4.341768,-1.894096 l 50.91788,-11.349167 -0.0113,53.144272 -34.733274,33.56547 z"
|
|
||||||
id="path958"
|
|
||||||
sodipodi:nodetypes="ccsccccc" /></g><g
|
|
||||||
inkscape:groupmode="layer"
|
|
||||||
id="layer3"
|
|
||||||
inkscape:label="Letter"
|
|
||||||
style="display:inline"
|
|
||||||
sodipodi:insensitive="true"
|
|
||||||
transform="translate(-51.420144,-44.470286)"><path
|
|
||||||
style="display:inline;fill:url(#linearGradient69600);fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
|
||||||
d="m 94.780881,91.406803 16.870379,17.074877 -23.723345,23.00249 -18.122131,-17.99816 5.497473,-2.80607 18.404054,-0.0511 2.35163,-8.23071 z"
|
|
||||||
id="path54894"
|
|
||||||
sodipodi:nodetypes="cccccccc" /><path
|
|
||||||
style="display:inline;fill:url(#linearGradient71930);fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
|
||||||
d="m 72.440405,92.224764 16.15467,15.745686 4.089788,-6.79927 z"
|
|
||||||
id="path54892" /><path
|
|
||||||
style="display:inline;fill:url(#linearGradient71922);fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
|
||||||
d="m 95.138739,84.965385 1.124691,-14.109776 22.92453,22.286787 0.008,8.164604 -3.28863,3.16649 z"
|
|
||||||
id="path54890"
|
|
||||||
sodipodi:nodetypes="cccccc"
|
|
||||||
inkscape:label="path54890" /><path
|
|
||||||
style="display:inline;fill:url(#linearGradient46363);fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
|
||||||
d="m 95.138739,84.965385 h 1.226936 l -0.05112,-14.109776 c 0,0 -5.776827,-3.220709 -12.167126,-2.40275 -6.390296,0.817957 -8.151582,2.1248 -10.58233,4.396523 -1.90229,1.777838 -2.913974,3.527446 -3.987546,7.157132 -0.512646,1.733226 -0.281963,5.988892 0.613471,8.537436 0.664591,1.891528 2.453873,4.294281 4.958868,6.134686 2.662335,1.956002 8.281825,3.527443 8.281825,3.527443 0,0 5.134614,1.887351 5.572338,4.294281 0.308137,1.69437 -0.102243,3.22071 -1.635914,4.95887 -1.258314,1.42609 -3.62969,1.99377 -6.288054,1.07357 -2.658364,-0.92021 -6.139514,-3.85065 -7.106009,-4.90775 -0.817958,-0.89464 -2.820665,-3.06173 -2.890231,-4.294021 -0.01209,-0.214205 -1.229505,-0.0963 -1.229505,-0.0963 l -0.0723,14.256941 5.879073,2.24938 c 0,0 5.214483,1.78929 8.946415,1.43143 3.731934,-0.35786 7.617235,-0.51122 11.604778,-5.16336 3.987542,-4.65213 3.680812,-12.831715 2.147141,-15.899056 -1.533673,-3.067344 -3.561212,-6.138812 -10.480087,-8.281826 -5.776829,-1.789283 -7.872846,-3.01622 -8.128458,-4.396524 -0.255611,-1.380305 0.0091,-4.253646 2.760607,-5.214481 3.220711,-1.124693 5.623462,-0.05112 7.05489,1.12469 1.431425,1.175817 5.572339,5.623462 5.572339,5.623462 z"
|
|
||||||
id="path46355"
|
|
||||||
sodipodi:nodetypes="cccssssscssssscccssssssscc" /></g></svg>
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 4.0 KiB |
@@ -1,31 +0,0 @@
|
|||||||
version: '3.3'
|
|
||||||
services:
|
|
||||||
stirling-pdf:
|
|
||||||
container_name: Stirling-PDF-Lite-Security
|
|
||||||
image: frooodle/s-pdf:latest-lite
|
|
||||||
deploy:
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
memory: 2G
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP' && curl -fL http://localhost:8080/ | grep -q 'Please sign in'"]
|
|
||||||
interval: 5s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 16
|
|
||||||
ports:
|
|
||||||
- 8080:8080
|
|
||||||
volumes:
|
|
||||||
- /stirling/latest/data:/usr/share/tesseract-ocr/5/tessdata:rw
|
|
||||||
- /stirling/latest/config:/configs:rw
|
|
||||||
- /stirling/latest/logs:/logs:rw
|
|
||||||
environment:
|
|
||||||
DOCKER_ENABLE_SECURITY: "true"
|
|
||||||
SECURITY_ENABLELOGIN: "true"
|
|
||||||
SYSTEM_DEFAULTLOCALE: en_US
|
|
||||||
UI_APPNAME: Stirling-PDF-Lite
|
|
||||||
UI_HOMEDESCRIPTION: Demo site for Stirling-PDF-Lite Latest with Security
|
|
||||||
UI_APPNAMENAVBAR: Stirling-PDF-Lite Latest
|
|
||||||
SYSTEM_MAXFILESIZE: "100"
|
|
||||||
METRICS_ENABLED: "true"
|
|
||||||
SYSTEM_GOOGLEVISIBILITY: "true"
|
|
||||||
restart: on-failure:5
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
version: '3.3'
|
|
||||||
services:
|
|
||||||
stirling-pdf:
|
|
||||||
container_name: Stirling-PDF-Lite
|
|
||||||
image: frooodle/s-pdf:latest-lite
|
|
||||||
deploy:
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
memory: 2G
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP' && curl -fL http://localhost:8080/ | grep -qv 'Please sign in'"]
|
|
||||||
interval: 5s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 16
|
|
||||||
ports:
|
|
||||||
- 8080:8080
|
|
||||||
volumes:
|
|
||||||
- /stirling/latest/config:/configs:rw
|
|
||||||
- /stirling/latest/logs:/logs:rw
|
|
||||||
environment:
|
|
||||||
DOCKER_ENABLE_SECURITY: "false"
|
|
||||||
SECURITY_ENABLELOGIN: "false"
|
|
||||||
SYSTEM_DEFAULTLOCALE: en_US
|
|
||||||
UI_APPNAME: Stirling-PDF-Lite
|
|
||||||
UI_HOMEDESCRIPTION: Demo site for Stirling-PDF-Lite Latest
|
|
||||||
UI_APPNAMENAVBAR: Stirling-PDF-Lite Latest
|
|
||||||
SYSTEM_MAXFILESIZE: "100"
|
|
||||||
METRICS_ENABLED: "true"
|
|
||||||
SYSTEM_GOOGLEVISIBILITY: "true"
|
|
||||||
restart: on-failure:5
|
|
||||||
39
exampleYmlFiles/docker-compose-latest-security-with-sso.yml
Normal file
39
exampleYmlFiles/docker-compose-latest-security-with-sso.yml
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
version: '3.3'
|
||||||
|
services:
|
||||||
|
stirling-pdf:
|
||||||
|
container_name: Stirling-PDF-Security
|
||||||
|
image: frooodle/s-pdf:latest
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 4G
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP' && curl -fL http://localhost:8080/ | grep -q 'Please sign in'"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 16
|
||||||
|
ports:
|
||||||
|
- 8080:8080
|
||||||
|
volumes:
|
||||||
|
- /stirling/latest/data:/usr/share/tessdata:rw
|
||||||
|
- /stirling/latest/config:/configs:rw
|
||||||
|
- /stirling/latest/logs:/logs:rw
|
||||||
|
environment:
|
||||||
|
DOCKER_ENABLE_SECURITY: "true"
|
||||||
|
SECURITY_ENABLELOGIN: "true"
|
||||||
|
SECURITY_OAUTH2_ENABLED: "true"
|
||||||
|
SECURITY_OAUTH2_AUTOCREATEUSER: "true" # This is set to true to allow auto-creation of non-existing users in Striling-PDF
|
||||||
|
SECURITY_OAUTH2_ISSUER: "https://accounts.google.com" # Change with any other provider that supports OpenID Connect Discovery (/.well-known/openid-configuration) end-point
|
||||||
|
SECURITY_OAUTH2_CLIENTID: "<YOUR CLIENT ID>.apps.googleusercontent.com" # Client ID from your provider
|
||||||
|
SECURITY_OAUTH2_CLIENTSECRET: "<YOUR CLIENT SECRET>" # Client Secret from your provider
|
||||||
|
PUID: 1002
|
||||||
|
PGID: 1002
|
||||||
|
UMASK: "022"
|
||||||
|
SYSTEM_DEFAULTLOCALE: en-US
|
||||||
|
UI_APPNAME: Stirling-PDF
|
||||||
|
UI_HOMEDESCRIPTION: Demo site for Stirling-PDF Latest with Security
|
||||||
|
UI_APPNAMENAVBAR: Stirling-PDF Latest
|
||||||
|
SYSTEM_MAXFILESIZE: "100"
|
||||||
|
METRICS_ENABLED: "true"
|
||||||
|
SYSTEM_GOOGLEVISIBILITY: "true"
|
||||||
|
restart: on-failure:5
|
||||||
@@ -15,13 +15,16 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- 8080:8080
|
- 8080:8080
|
||||||
volumes:
|
volumes:
|
||||||
- /stirling/latest/data:/usr/share/tesseract-ocr/5/tessdata:rw
|
- /stirling/latest/data:/usr/share/tessdata:rw
|
||||||
- /stirling/latest/config:/configs:rw
|
- /stirling/latest/config:/configs:rw
|
||||||
- /stirling/latest/logs:/logs:rw
|
- /stirling/latest/logs:/logs:rw
|
||||||
environment:
|
environment:
|
||||||
DOCKER_ENABLE_SECURITY: "true"
|
DOCKER_ENABLE_SECURITY: "true"
|
||||||
SECURITY_ENABLELOGIN: "true"
|
SECURITY_ENABLELOGIN: "true"
|
||||||
SYSTEM_DEFAULTLOCALE: en_US
|
PUID: 1002
|
||||||
|
PGID: 1002
|
||||||
|
UMASK: "022"
|
||||||
|
SYSTEM_DEFAULTLOCALE: en-US
|
||||||
UI_APPNAME: Stirling-PDF
|
UI_APPNAME: Stirling-PDF
|
||||||
UI_HOMEDESCRIPTION: Demo site for Stirling-PDF Latest with Security
|
UI_HOMEDESCRIPTION: Demo site for Stirling-PDF Latest with Security
|
||||||
UI_APPNAMENAVBAR: Stirling-PDF Latest
|
UI_APPNAMENAVBAR: Stirling-PDF Latest
|
||||||
|
|||||||
@@ -15,13 +15,13 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- 8080:8080
|
- 8080:8080
|
||||||
volumes:
|
volumes:
|
||||||
- /stirling/latest/data:/usr/share/tesseract-ocr/5/tessdata:rw
|
- /stirling/latest/data:/usr/share/tessdata:rw
|
||||||
- /stirling/latest/config:/configs:rw
|
- /stirling/latest/config:/configs:rw
|
||||||
- /stirling/latest/logs:/logs:rw
|
- /stirling/latest/logs:/logs:rw
|
||||||
environment:
|
environment:
|
||||||
DOCKER_ENABLE_SECURITY: "true"
|
DOCKER_ENABLE_SECURITY: "true"
|
||||||
SECURITY_ENABLELOGIN: "true"
|
SECURITY_ENABLELOGIN: "true"
|
||||||
SYSTEM_DEFAULTLOCALE: en_US
|
SYSTEM_DEFAULTLOCALE: en-US
|
||||||
UI_APPNAME: Stirling-PDF-Lite
|
UI_APPNAME: Stirling-PDF-Lite
|
||||||
UI_HOMEDESCRIPTION: Demo site for Stirling-PDF-Lite Latest with Security
|
UI_HOMEDESCRIPTION: Demo site for Stirling-PDF-Lite Latest with Security
|
||||||
UI_APPNAMENAVBAR: Stirling-PDF-Lite Latest
|
UI_APPNAMENAVBAR: Stirling-PDF-Lite Latest
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
DOCKER_ENABLE_SECURITY: "false"
|
DOCKER_ENABLE_SECURITY: "false"
|
||||||
SECURITY_ENABLELOGIN: "false"
|
SECURITY_ENABLELOGIN: "false"
|
||||||
SYSTEM_DEFAULTLOCALE: en_US
|
SYSTEM_DEFAULTLOCALE: en-US
|
||||||
UI_APPNAME: Stirling-PDF-Ultra-lite
|
UI_APPNAME: Stirling-PDF-Ultra-lite
|
||||||
UI_HOMEDESCRIPTION: Demo site for Stirling-PDF-Ultra-lite Latest
|
UI_HOMEDESCRIPTION: Demo site for Stirling-PDF-Ultra-lite Latest
|
||||||
UI_APPNAMENAVBAR: Stirling-PDF-Ultra-lite Latest
|
UI_APPNAMENAVBAR: Stirling-PDF-Ultra-lite Latest
|
||||||
|
|||||||
@@ -15,13 +15,15 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- 8080:8080
|
- 8080:8080
|
||||||
volumes:
|
volumes:
|
||||||
- /stirling/latest/data:/usr/share/tesseract-ocr/5/tessdata:rw
|
- /stirling/latest/data:/usr/share/tessdata:rw
|
||||||
- /stirling/latest/config:/configs:rw
|
- /stirling/latest/config:/configs:rw
|
||||||
- /stirling/latest/logs:/logs:rw
|
- /stirling/latest/logs:/logs:rw
|
||||||
environment:
|
environment:
|
||||||
DOCKER_ENABLE_SECURITY: "false"
|
DOCKER_ENABLE_SECURITY: "false"
|
||||||
SECURITY_ENABLELOGIN: "false"
|
SECURITY_ENABLELOGIN: "false"
|
||||||
SYSTEM_DEFAULTLOCALE: en_US
|
LANGS: "en_GB,en_US,ar_AR,de_DE,fr_FR,es_ES,zh_CN,zh_TW,ca_CA,it_IT,sv_SE,pl_PL,ro_RO,ko_KR,pt_BR,ru_RU,el_GR,hi_IN,hu_HU,tr_TR,id_ID"
|
||||||
|
INSTALL_BOOK_AND_ADVANCED_HTML_OPS: "true"
|
||||||
|
SYSTEM_DEFAULTLOCALE: en-US
|
||||||
UI_APPNAME: Stirling-PDF
|
UI_APPNAME: Stirling-PDF
|
||||||
UI_HOMEDESCRIPTION: Demo site for Stirling-PDF Latest
|
UI_HOMEDESCRIPTION: Demo site for Stirling-PDF Latest
|
||||||
UI_APPNAMENAVBAR: Stirling-PDF Latest
|
UI_APPNAMENAVBAR: Stirling-PDF Latest
|
||||||
|
|||||||
182
gradlew.bat
vendored
182
gradlew.bat
vendored
@@ -1,91 +1,91 @@
|
|||||||
@rem
|
@rem
|
||||||
@rem Copyright 2015 the original author or authors.
|
@rem Copyright 2015 the original author or authors.
|
||||||
@rem
|
@rem
|
||||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
@rem you may not use this file except in compliance with the License.
|
@rem you may not use this file except in compliance with the License.
|
||||||
@rem You may obtain a copy of the License at
|
@rem You may obtain a copy of the License at
|
||||||
@rem
|
@rem
|
||||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||||
@rem
|
@rem
|
||||||
@rem Unless required by applicable law or agreed to in writing, software
|
@rem Unless required by applicable law or agreed to in writing, software
|
||||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
@rem See the License for the specific language governing permissions and
|
@rem See the License for the specific language governing permissions and
|
||||||
@rem limitations under the License.
|
@rem limitations under the License.
|
||||||
@rem
|
@rem
|
||||||
|
|
||||||
@if "%DEBUG%"=="" @echo off
|
@if "%DEBUG%"=="" @echo off
|
||||||
@rem ##########################################################################
|
@rem ##########################################################################
|
||||||
@rem
|
@rem
|
||||||
@rem Gradle startup script for Windows
|
@rem Gradle startup script for Windows
|
||||||
@rem
|
@rem
|
||||||
@rem ##########################################################################
|
@rem ##########################################################################
|
||||||
|
|
||||||
@rem Set local scope for the variables with windows NT shell
|
@rem Set local scope for the variables with windows NT shell
|
||||||
if "%OS%"=="Windows_NT" setlocal
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
set DIRNAME=%~dp0
|
set DIRNAME=%~dp0
|
||||||
if "%DIRNAME%"=="" set DIRNAME=.
|
if "%DIRNAME%"=="" set DIRNAME=.
|
||||||
set APP_BASE_NAME=%~n0
|
set APP_BASE_NAME=%~n0
|
||||||
set APP_HOME=%DIRNAME%
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||||
|
|
||||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||||
|
|
||||||
@rem Find java.exe
|
@rem Find java.exe
|
||||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
set JAVA_EXE=java.exe
|
set JAVA_EXE=java.exe
|
||||||
%JAVA_EXE% -version >NUL 2>&1
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
if %ERRORLEVEL% equ 0 goto execute
|
if %ERRORLEVEL% equ 0 goto execute
|
||||||
|
|
||||||
echo.
|
echo.
|
||||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
echo.
|
echo.
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
echo location of your Java installation.
|
echo location of your Java installation.
|
||||||
|
|
||||||
goto fail
|
goto fail
|
||||||
|
|
||||||
:findJavaFromJavaHome
|
:findJavaFromJavaHome
|
||||||
set JAVA_HOME=%JAVA_HOME:"=%
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
if exist "%JAVA_EXE%" goto execute
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
echo.
|
echo.
|
||||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||||
echo.
|
echo.
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
echo location of your Java installation.
|
echo location of your Java installation.
|
||||||
|
|
||||||
goto fail
|
goto fail
|
||||||
|
|
||||||
:execute
|
:execute
|
||||||
@rem Setup the command line
|
@rem Setup the command line
|
||||||
|
|
||||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
@rem Execute Gradle
|
@rem Execute Gradle
|
||||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||||
|
|
||||||
:end
|
:end
|
||||||
@rem End local scope for the variables with windows NT shell
|
@rem End local scope for the variables with windows NT shell
|
||||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||||
|
|
||||||
:fail
|
:fail
|
||||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
rem the _cmd.exe /c_ return code!
|
rem the _cmd.exe /c_ return code!
|
||||||
set EXIT_CODE=%ERRORLEVEL%
|
set EXIT_CODE=%ERRORLEVEL%
|
||||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||||
exit /b %EXIT_CODE%
|
exit /b %EXIT_CODE%
|
||||||
|
|
||||||
:mainEnd
|
:mainEnd
|
||||||
if "%OS%"=="Windows_NT" endlocal
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
:omega
|
:omega
|
||||||
|
|||||||
BIN
images/stirling-home.jpg
Normal file
BIN
images/stirling-home.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 203 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 131 KiB |
@@ -6,7 +6,8 @@
|
|||||||
"parameters": {
|
"parameters": {
|
||||||
"horizontalDivisions": 2,
|
"horizontalDivisions": 2,
|
||||||
"verticalDivisions": 2,
|
"verticalDivisions": 2,
|
||||||
"fileInput": "automated"
|
"fileInput": "automated",
|
||||||
|
"merge": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -30,4 +31,4 @@
|
|||||||
},
|
},
|
||||||
"outputDir": "{outputFolder}",
|
"outputDir": "{outputFolder}",
|
||||||
"outputFileName": "{filename}"
|
"outputFileName": "{filename}"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ public class PropSync {
|
|||||||
Map<String, String> enProps = linesToProps(enLines);
|
Map<String, String> enProps = linesToProps(enLines);
|
||||||
|
|
||||||
for (File file : files) {
|
for (File file : files) {
|
||||||
if (!file.getName().equals("messages_en_GB.properties")) {
|
if (!"messages_en_GB.properties".equals(file.getName())) {
|
||||||
System.out.println("Processing file: " + file.getName());
|
System.out.println("Processing file: " + file.getName());
|
||||||
List<String> lines;
|
List<String> lines;
|
||||||
try {
|
try {
|
||||||
|
|||||||
192
scripts/counter_translation.py
Normal file
192
scripts/counter_translation.py
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
"""A script to update language progress status in README.md based on
|
||||||
|
properties file comparison.
|
||||||
|
|
||||||
|
This script compares default properties file with others in a directory to
|
||||||
|
determine language progress.
|
||||||
|
It then updates README.md based on provided progress list.
|
||||||
|
|
||||||
|
Author: Ludy87
|
||||||
|
|
||||||
|
Example:
|
||||||
|
To use this script, simply run it from command line:
|
||||||
|
$ python counter_translation.py
|
||||||
|
""" # noqa: D205
|
||||||
|
|
||||||
|
import glob
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
|
import tomlkit
|
||||||
|
import tomlkit.toml_file
|
||||||
|
|
||||||
|
|
||||||
|
def convert_to_multiline(data: tomlkit.TOMLDocument) -> tomlkit.TOMLDocument:
|
||||||
|
"""Converts 'ignore' and 'missing' arrays to multiline arrays and sorts the first-level keys of the TOML document.
|
||||||
|
Enhances readability and consistency in the TOML file by ensuring arrays contain unique and sorted entries.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
data (tomlkit.TOMLDocument): The original TOML document containing the data.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tomlkit.TOMLDocument: A new TOML document with sorted keys and properly formatted arrays.
|
||||||
|
""" # noqa: D205
|
||||||
|
sorted_data = tomlkit.document()
|
||||||
|
for key in sorted(data.keys()):
|
||||||
|
value = data[key]
|
||||||
|
if isinstance(value, dict):
|
||||||
|
new_table = tomlkit.table()
|
||||||
|
for subkey in ("ignore", "missing"):
|
||||||
|
if subkey in value:
|
||||||
|
# Convert the list to a set to remove duplicates, sort it, and convert to multiline for readability
|
||||||
|
unique_sorted_array = sorted(set(value[subkey]))
|
||||||
|
array = tomlkit.array()
|
||||||
|
array.multiline(True)
|
||||||
|
for item in unique_sorted_array:
|
||||||
|
array.append(item)
|
||||||
|
new_table[subkey] = array
|
||||||
|
sorted_data[key] = new_table
|
||||||
|
else:
|
||||||
|
# Add other types of data unchanged
|
||||||
|
sorted_data[key] = value
|
||||||
|
return sorted_data
|
||||||
|
|
||||||
|
|
||||||
|
def write_readme(progress_list: list[tuple[str, int]]) -> None:
|
||||||
|
"""Updates the progress status in the README.md file based
|
||||||
|
on the provided progress list.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
progress_list (list[tuple[str, int]]): A list of tuples containing
|
||||||
|
language and progress percentage.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None
|
||||||
|
""" # noqa: D205
|
||||||
|
with open("README.md", encoding="utf-8") as file:
|
||||||
|
content = file.readlines()
|
||||||
|
|
||||||
|
for i, line in enumerate(content[2:], start=2):
|
||||||
|
for progress in progress_list:
|
||||||
|
language, value = progress
|
||||||
|
if language in line:
|
||||||
|
if match := re.search(r"\!\[(\d+(\.\d+)?)%\]\(.*\)", line):
|
||||||
|
content[i] = line.replace(
|
||||||
|
match.group(0),
|
||||||
|
f"",
|
||||||
|
)
|
||||||
|
|
||||||
|
with open("README.md", "w", encoding="utf-8") as file:
|
||||||
|
file.writelines(content)
|
||||||
|
|
||||||
|
|
||||||
|
def compare_files(default_file_path, file_paths, translation_status_file) -> list[tuple[str, int]]:
|
||||||
|
"""Compares the default properties file with other
|
||||||
|
properties files in the directory.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
default_file_path (str): The path to the default properties file.
|
||||||
|
files_directory (str): The directory containing other properties files.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[tuple[str, int]]: A list of tuples containing
|
||||||
|
language and progress percentage.
|
||||||
|
""" # noqa: D205
|
||||||
|
num_lines = sum(
|
||||||
|
1 for line in open(default_file_path, encoding="utf-8") if line.strip() and not line.strip().startswith("#")
|
||||||
|
)
|
||||||
|
|
||||||
|
result_list = []
|
||||||
|
sort_translation_status: tomlkit.TOMLDocument
|
||||||
|
|
||||||
|
# read toml
|
||||||
|
with open(translation_status_file, encoding="utf-8") as f:
|
||||||
|
sort_translation_status = tomlkit.parse(f.read())
|
||||||
|
|
||||||
|
for file_path in file_paths:
|
||||||
|
language = os.path.basename(file_path).split("messages_", 1)[1].split(".properties", 1)[0]
|
||||||
|
|
||||||
|
fails = 0
|
||||||
|
if "en_GB" in language or "en_US" in language:
|
||||||
|
result_list.append(("en_GB", 100))
|
||||||
|
result_list.append(("en_US", 100))
|
||||||
|
continue
|
||||||
|
|
||||||
|
if language not in sort_translation_status:
|
||||||
|
sort_translation_status[language] = tomlkit.table()
|
||||||
|
|
||||||
|
if (
|
||||||
|
"ignore" not in sort_translation_status[language]
|
||||||
|
or len(sort_translation_status[language].get("ignore", [])) < 1
|
||||||
|
):
|
||||||
|
sort_translation_status[language]["ignore"] = tomlkit.array(["language.direction"])
|
||||||
|
|
||||||
|
# if "missing" not in sort_translation_status[language]:
|
||||||
|
# sort_translation_status[language]["missing"] = tomlkit.array()
|
||||||
|
# elif "language.direction" in sort_translation_status[language]["missing"]:
|
||||||
|
# sort_translation_status[language]["missing"].remove("language.direction")
|
||||||
|
|
||||||
|
with open(default_file_path, encoding="utf-8") as default_file, open(file_path, encoding="utf-8") as file:
|
||||||
|
for _ in range(5):
|
||||||
|
next(default_file)
|
||||||
|
try:
|
||||||
|
next(file)
|
||||||
|
except StopIteration:
|
||||||
|
fails = num_lines
|
||||||
|
|
||||||
|
for line_num, (line_default, line_file) in enumerate(zip(default_file, file), start=6):
|
||||||
|
try:
|
||||||
|
# Ignoring empty lines and lines start with #
|
||||||
|
if line_default.strip() == "" or line_default.startswith("#"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
default_key, default_value = line_default.split("=", 1)
|
||||||
|
file_key, file_value = line_file.split("=", 1)
|
||||||
|
if (
|
||||||
|
default_value.strip() == file_value.strip()
|
||||||
|
and default_key.strip() not in sort_translation_status[language]["ignore"]
|
||||||
|
):
|
||||||
|
print(f"{language}: Line {line_num} is missing the translation.")
|
||||||
|
# if default_key.strip() not in sort_translation_status[language]["missing"]:
|
||||||
|
# missing_array = tomlkit.array()
|
||||||
|
# missing_array.append(default_key.strip())
|
||||||
|
# missing_array.multiline(True)
|
||||||
|
# sort_translation_status[language]["missing"].extend(missing_array)
|
||||||
|
fails += 1
|
||||||
|
# elif default_key.strip() in sort_translation_status[language]["ignore"]:
|
||||||
|
# if default_key.strip() in sort_translation_status[language]["missing"]:
|
||||||
|
# sort_translation_status[language]["missing"].remove(default_key.strip())
|
||||||
|
if default_value.strip() != file_value.strip():
|
||||||
|
# if default_key.strip() in sort_translation_status[language]["missing"]:
|
||||||
|
# sort_translation_status[language]["missing"].remove(default_key.strip())
|
||||||
|
if default_key.strip() in sort_translation_status[language]["ignore"]:
|
||||||
|
sort_translation_status[language]["ignore"].remove(default_key.strip())
|
||||||
|
|
||||||
|
except IndexError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
print(f"{language}: {fails} out of {num_lines} lines are not translated.")
|
||||||
|
result_list.append(
|
||||||
|
(
|
||||||
|
language,
|
||||||
|
int((num_lines - fails) * 100 / num_lines),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
translation_status = convert_to_multiline(sort_translation_status)
|
||||||
|
with open(translation_status_file, "w", encoding="utf-8") as file:
|
||||||
|
file.write(tomlkit.dumps(translation_status))
|
||||||
|
|
||||||
|
unique_data = list(set(result_list))
|
||||||
|
unique_data.sort(key=lambda x: x[1], reverse=True)
|
||||||
|
|
||||||
|
return unique_data
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
directory = os.path.join(os.getcwd(), "src", "main", "resources")
|
||||||
|
messages_file_paths = glob.glob(os.path.join(directory, "messages_*.properties"))
|
||||||
|
reference_file = os.path.join(directory, "messages_en_GB.properties")
|
||||||
|
|
||||||
|
scripts_directory = os.path.join(os.getcwd(), "scripts")
|
||||||
|
translation_state_file = os.path.join(scripts_directory, "translation_status.toml")
|
||||||
|
|
||||||
|
write_readme(compare_files(reference_file, messages_file_paths, translation_state_file))
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
import cv2
|
|
||||||
import sys
|
|
||||||
import argparse
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
def is_blank_image(image_path, threshold=10, white_percent=99, white_value=255, blur_size=5):
|
|
||||||
image = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
|
|
||||||
|
|
||||||
if image is None:
|
|
||||||
print(f"Error: Unable to read the image file: {image_path}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Apply Gaussian blur to reduce noise
|
|
||||||
blurred_image = cv2.GaussianBlur(image, (blur_size, blur_size), 0)
|
|
||||||
|
|
||||||
_, thresholded_image = cv2.threshold(blurred_image, white_value - threshold, white_value, cv2.THRESH_BINARY)
|
|
||||||
|
|
||||||
# Calculate the percentage of white pixels in the thresholded image
|
|
||||||
white_pixels = np.sum(thresholded_image == white_value)
|
|
||||||
white_pixel_percentage = (white_pixels / thresholded_image.size) * 100
|
|
||||||
|
|
||||||
print(f"Page has white pixel percent of {white_pixel_percentage}")
|
|
||||||
return white_pixel_percentage >= white_percent
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
parser = argparse.ArgumentParser(description='Detect if an image is considered blank or not.')
|
|
||||||
parser.add_argument('image_path', help='The path to the image file.')
|
|
||||||
parser.add_argument('-t', '--threshold', type=int, default=10, help='Threshold for determining white pixels. The default value is 10.')
|
|
||||||
parser.add_argument('-w', '--white_percent', type=float, default=99, help='The percentage of white pixels for an image to be considered blank. The default value is 99.')
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
blank = is_blank_image(args.image_path, args.threshold, args.white_percent)
|
|
||||||
|
|
||||||
# Return code 1: The image is considered blank.
|
|
||||||
# Return code 0: The image is not considered blank.
|
|
||||||
sys.exit(int(blank))
|
|
||||||
@@ -4,7 +4,7 @@ if [ "$DOCKER_ENABLE_SECURITY" = "true" ] && [ "$VERSION_TAG" != "alpha" ]; then
|
|||||||
if [ ! -f app-security.jar ]; then
|
if [ ! -f app-security.jar ]; then
|
||||||
echo "Trying to download from: https://github.com/Stirling-Tools/Stirling-PDF/releases/download/v$VERSION_TAG/Stirling-PDF-with-login.jar"
|
echo "Trying to download from: https://github.com/Stirling-Tools/Stirling-PDF/releases/download/v$VERSION_TAG/Stirling-PDF-with-login.jar"
|
||||||
curl -L -o app-security.jar https://github.com/Stirling-Tools/Stirling-PDF/releases/download/v$VERSION_TAG/Stirling-PDF-with-login.jar
|
curl -L -o app-security.jar https://github.com/Stirling-Tools/Stirling-PDF/releases/download/v$VERSION_TAG/Stirling-PDF-with-login.jar
|
||||||
|
|
||||||
# If the first download attempt failed, try with the 'v' prefix
|
# If the first download attempt failed, try with the 'v' prefix
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
echo "Trying to download from: https://github.com/Stirling-Tools/Stirling-PDF/releases/download/$VERSION_TAG/Stirling-PDF-with-login.jar"
|
echo "Trying to download from: https://github.com/Stirling-Tools/Stirling-PDF/releases/download/$VERSION_TAG/Stirling-PDF-with-login.jar"
|
||||||
@@ -14,6 +14,8 @@ if [ "$DOCKER_ENABLE_SECURITY" = "true" ] && [ "$VERSION_TAG" != "alpha" ]; then
|
|||||||
if [ $? -eq 0 ]; then # checks if curl was successful
|
if [ $? -eq 0 ]; then # checks if curl was successful
|
||||||
rm -f app.jar
|
rm -f app.jar
|
||||||
ln -s app-security.jar app.jar
|
ln -s app-security.jar app.jar
|
||||||
|
chown stirlingpdfuser:stirlingpdfgroup app.jar || true
|
||||||
|
chmod 755 app.jar || true
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -1,6 +1,34 @@
|
|||||||
#!/bin/sh
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Update the user and group IDs as per environment variables
|
||||||
|
if [ ! -z "$PUID" ] && [ "$PUID" != "$(id -u stirlingpdfuser)" ]; then
|
||||||
|
usermod -o -u "$PUID" stirlingpdfuser || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
if [ ! -z "$PGID" ] && [ "$PGID" != "$(getent group stirlingpdfgroup | cut -d: -f3)" ]; then
|
||||||
|
groupmod -o -g "$PGID" stirlingpdfgroup || true
|
||||||
|
fi
|
||||||
|
umask "$UMASK" || true
|
||||||
|
|
||||||
|
if [[ "$INSTALL_BOOK_AND_ADVANCED_HTML_OPS" == "true" ]]; then
|
||||||
|
apk add --no-cache calibre@testing
|
||||||
|
fi
|
||||||
|
|
||||||
/scripts/download-security-jar.sh
|
/scripts/download-security-jar.sh
|
||||||
|
|
||||||
# Run the main command
|
if [[ -n "$LANGS" ]]; then
|
||||||
exec "$@"
|
/scripts/installFonts.sh $LANGS
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Setting permissions and ownership for necessary directories..."
|
||||||
|
# Attempt to change ownership of directories and files
|
||||||
|
if chown -R stirlingpdfuser:stirlingpdfgroup $HOME /logs /scripts /usr/share/fonts/opentype/noto /configs /customFiles /pipeline /app.jar; then
|
||||||
|
chmod -R 755 /logs /scripts /usr/share/fonts/opentype/noto /configs /customFiles /pipeline /app.jar || true
|
||||||
|
# If chown succeeds, execute the command as stirlingpdfuser
|
||||||
|
exec su-exec stirlingpdfuser "$@"
|
||||||
|
else
|
||||||
|
# If chown fails, execute the command without changing the user context
|
||||||
|
echo "[WARN] Chown failed, running as host user"
|
||||||
|
exec "$@"
|
||||||
|
fi
|
||||||
|
|||||||
@@ -2,25 +2,30 @@
|
|||||||
|
|
||||||
# Copy the original tesseract-ocr files to the volume directory without overwriting existing files
|
# Copy the original tesseract-ocr files to the volume directory without overwriting existing files
|
||||||
echo "Copying original files without overwriting existing files"
|
echo "Copying original files without overwriting existing files"
|
||||||
mkdir -p /usr/share/tesseract-ocr
|
mkdir -p /usr/share/tessdata
|
||||||
cp -rn /usr/share/tesseract-ocr-original/* /usr/share/tesseract-ocr
|
cp -rn /usr/share/tessdata-original/* /usr/share/tessdata
|
||||||
|
|
||||||
if [ -d /usr/share/tesseract-ocr/4.00/tessdata ]; then
|
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;
|
cp -r /usr/share/tesseract-ocr/4.00/tessdata/* /usr/share/tessdata || true;
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -d /usr/share/tesseract-ocr/5/tessdata ]; then
|
||||||
|
cp -r /usr/share/tesseract-ocr/5/tessdata/* /usr/share/tessdata || true;
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check if TESSERACT_LANGS environment variable is set and is not empty
|
# Check if TESSERACT_LANGS environment variable is set and is not empty
|
||||||
if [[ -n "$TESSERACT_LANGS" ]]; then
|
if [[ -n "$TESSERACT_LANGS" ]]; then
|
||||||
# Convert comma-separated values to a space-separated list
|
# Convert comma-separated values to a space-separated list
|
||||||
LANGS=$(echo $TESSERACT_LANGS | tr ',' ' ')
|
LANGS=$(echo $TESSERACT_LANGS | tr ',' ' ')
|
||||||
|
pattern='^[a-zA-Z]{2,4}(_[a-zA-Z]{2,4})?$'
|
||||||
# Install each language pack
|
# Install each language pack
|
||||||
for LANG in $LANGS; do
|
for LANG in $LANGS; do
|
||||||
apt-get install -y "tesseract-ocr-$LANG"
|
if [[ $LANG =~ $pattern ]]; then
|
||||||
|
apk add --no-cache "tesseract-ocr-data-$LANG"
|
||||||
|
else
|
||||||
|
echo "Skipping invalid language code"
|
||||||
|
fi
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
/scripts/download-security-jar.sh
|
/scripts/init-without-ocr.sh "$@"
|
||||||
|
|
||||||
# Run the main command
|
|
||||||
exec "$@"
|
|
||||||
67
scripts/installFonts.sh
Normal file
67
scripts/installFonts.sh
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
LANGS=$1
|
||||||
|
|
||||||
|
# Function to install a font package
|
||||||
|
install_font() {
|
||||||
|
echo "Installing font package: $1"
|
||||||
|
if ! apk add "$1" --no-cache; then
|
||||||
|
echo "Failed to install $1"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Install common fonts used across many languages
|
||||||
|
#common_fonts=(
|
||||||
|
# font-terminus
|
||||||
|
# font-dejavu
|
||||||
|
# font-noto
|
||||||
|
# font-noto-cjk
|
||||||
|
# font-awesome
|
||||||
|
# font-noto-extra
|
||||||
|
#)
|
||||||
|
#
|
||||||
|
#for font in "${common_fonts[@]}"; do
|
||||||
|
# install_font $font
|
||||||
|
#done
|
||||||
|
|
||||||
|
# Map languages to specific font packages
|
||||||
|
declare -A language_fonts=(
|
||||||
|
["ar_AR"]="font-noto-arabic"
|
||||||
|
["zh_CN"]="font-isas-misc"
|
||||||
|
["zh_TW"]="font-isas-misc"
|
||||||
|
["ja_JP"]="font-noto font-noto-thai font-noto-tibetan font-ipa font-sony-misc font-jis-misc"
|
||||||
|
["ru_RU"]="font-vollkorn font-misc-cyrillic font-mutt-misc font-screen-cyrillic font-winitzki-cyrillic font-cronyx-cyrillic"
|
||||||
|
["sr_LATN_RS"]="font-vollkorn font-misc-cyrillic font-mutt-misc font-screen-cyrillic font-winitzki-cyrillic font-cronyx-cyrillic"
|
||||||
|
["uk_UA"]="font-vollkorn font-misc-cyrillic font-mutt-misc font-screen-cyrillic font-winitzki-cyrillic font-cronyx-cyrillic"
|
||||||
|
["ko_KR"]="font-noto font-noto-thai font-noto-tibetan"
|
||||||
|
["el_GR"]="font-noto"
|
||||||
|
["hi_IN"]="font-noto-devanagari"
|
||||||
|
["bg_BG"]="font-vollkorn font-misc-cyrillic"
|
||||||
|
["GENERAL"]="font-terminus font-dejavu font-noto font-noto-cjk font-awesome font-noto-extra"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Install fonts for other languages which generally do not need special packages beyond 'font-noto'
|
||||||
|
other_langs=("en_GB" "en_US" "de_DE" "fr_FR" "es_ES" "ca_CA" "it_IT" "pt_BR" "nl_NL" "sv_SE" "pl_PL" "ro_RO" "hu_HU" "tr_TR" "id_ID" "eu_ES")
|
||||||
|
if [[ $LANGS == "ALL" ]]; then
|
||||||
|
# Install all fonts from the language_fonts map
|
||||||
|
for fonts in "${language_fonts[@]}"; do
|
||||||
|
for font in $fonts; do
|
||||||
|
install_font $font
|
||||||
|
done
|
||||||
|
done
|
||||||
|
else
|
||||||
|
# Split comma-separated languages and install necessary fonts
|
||||||
|
IFS=',' read -ra LANG_CODES <<< "$LANGS"
|
||||||
|
for code in "${LANG_CODES[@]}"; do
|
||||||
|
if [[ " ${other_langs[@]} " =~ " ${code} " ]]; then
|
||||||
|
install_font font-noto
|
||||||
|
else
|
||||||
|
fonts_to_install=${language_fonts[$code]}
|
||||||
|
if [ ! -z "$fonts_to_install" ]; then
|
||||||
|
for font in $fonts_to_install; do
|
||||||
|
install_font $font
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
@@ -2,7 +2,7 @@ import argparse
|
|||||||
import sys
|
import sys
|
||||||
import cv2
|
import cv2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import os
|
import os
|
||||||
|
|
||||||
def find_photo_boundaries(image, background_color, tolerance=30, min_area=10000, min_contour_area=500):
|
def find_photo_boundaries(image, background_color, tolerance=30, min_area=10000, min_contour_area=500):
|
||||||
mask = cv2.inRange(image, background_color - tolerance, background_color + tolerance)
|
mask = cv2.inRange(image, background_color - tolerance, background_color + tolerance)
|
||||||
@@ -49,9 +49,9 @@ def auto_rotate(image, angle_threshold=1):
|
|||||||
angles = []
|
angles = []
|
||||||
for rho, theta in lines[:, 0]:
|
for rho, theta in lines[:, 0]:
|
||||||
angles.append((theta * 180) / np.pi - 90)
|
angles.append((theta * 180) / np.pi - 90)
|
||||||
|
|
||||||
angle = np.median(angles)
|
angle = np.median(angles)
|
||||||
|
|
||||||
if abs(angle) < angle_threshold:
|
if abs(angle) < angle_threshold:
|
||||||
return image
|
return image
|
||||||
|
|
||||||
@@ -65,16 +65,16 @@ def auto_rotate(image, angle_threshold=1):
|
|||||||
|
|
||||||
def crop_borders(image, border_color, tolerance=30):
|
def crop_borders(image, border_color, tolerance=30):
|
||||||
mask = cv2.inRange(image, border_color - tolerance, border_color + tolerance)
|
mask = cv2.inRange(image, border_color - tolerance, border_color + tolerance)
|
||||||
|
|
||||||
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
||||||
if len(contours) == 0:
|
if len(contours) == 0:
|
||||||
return image
|
return image
|
||||||
|
|
||||||
largest_contour = max(contours, key=cv2.contourArea)
|
largest_contour = max(contours, key=cv2.contourArea)
|
||||||
x, y, w, h = cv2.boundingRect(largest_contour)
|
x, y, w, h = cv2.boundingRect(largest_contour)
|
||||||
|
|
||||||
return image[y:y+h, x:x+w]
|
return image[y:y+h, x:x+w]
|
||||||
|
|
||||||
def split_photos(input_file, output_directory, tolerance=30, min_area=10000, min_contour_area=500, angle_threshold=10, border_size=0):
|
def split_photos(input_file, output_directory, tolerance=30, min_area=10000, min_contour_area=500, angle_threshold=10, border_size=0):
|
||||||
image = cv2.imread(input_file)
|
image = cv2.imread(input_file)
|
||||||
background_color = estimate_background_color(image)
|
background_color = estimate_background_color(image)
|
||||||
@@ -110,7 +110,7 @@ if __name__ == "__main__":
|
|||||||
parser.add_argument("--min_contour_area", type=int, default=500, help="Sets the minimum contour area threshold for a photo (default: 500).")
|
parser.add_argument("--min_contour_area", type=int, default=500, help="Sets the minimum contour area threshold for a photo (default: 500).")
|
||||||
parser.add_argument("--angle_threshold", type=int, default=10, help="Sets the minimum absolute angle required for the image to be rotated (default: 10).")
|
parser.add_argument("--angle_threshold", type=int, default=10, help="Sets the minimum absolute angle required for the image to be rotated (default: 10).")
|
||||||
parser.add_argument("--border_size", type=int, default=0, help="Sets the size of the border added and removed to prevent white borders in the output (default: 0).")
|
parser.add_argument("--border_size", type=int, default=0, help="Sets the size of the border added and removed to prevent white borders in the output (default: 0).")
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
split_photos(args.input_file, args.output_directory, tolerance=args.tolerance, min_area=args.min_area, min_contour_area=args.min_contour_area, angle_threshold=args.angle_threshold, border_size=args.border_size)
|
split_photos(args.input_file, args.output_directory, tolerance=args.tolerance, min_area=args.min_area, min_contour_area=args.min_contour_area, angle_threshold=args.angle_threshold, border_size=args.border_size)
|
||||||
|
|||||||
159
scripts/translation_status.toml
Normal file
159
scripts/translation_status.toml
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
[ar_AR]
|
||||||
|
ignore = [
|
||||||
|
'language.direction',
|
||||||
|
]
|
||||||
|
|
||||||
|
[bg_BG]
|
||||||
|
ignore = [
|
||||||
|
'language.direction',
|
||||||
|
]
|
||||||
|
|
||||||
|
[ca_CA]
|
||||||
|
ignore = [
|
||||||
|
'language.direction',
|
||||||
|
]
|
||||||
|
|
||||||
|
[de_DE]
|
||||||
|
ignore = [
|
||||||
|
'AddStampRequest.alphabet',
|
||||||
|
'AddStampRequest.position',
|
||||||
|
'PDFToBook.selectText.1',
|
||||||
|
'PDFToText.tags',
|
||||||
|
'addPageNumbers.selectText.3',
|
||||||
|
'alphabet',
|
||||||
|
'certSign.name',
|
||||||
|
'language.direction',
|
||||||
|
'licenses.version',
|
||||||
|
'pipeline.title',
|
||||||
|
'pipelineOptions.pipelineHeader',
|
||||||
|
'sponsor',
|
||||||
|
'text',
|
||||||
|
'watermark.type.1',
|
||||||
|
]
|
||||||
|
|
||||||
|
[el_GR]
|
||||||
|
ignore = [
|
||||||
|
'language.direction',
|
||||||
|
]
|
||||||
|
|
||||||
|
[es_ES]
|
||||||
|
ignore = [
|
||||||
|
'adminUserSettings.roles',
|
||||||
|
'color',
|
||||||
|
'language.direction',
|
||||||
|
'no',
|
||||||
|
'showJS.tags',
|
||||||
|
]
|
||||||
|
|
||||||
|
[eu_ES]
|
||||||
|
ignore = [
|
||||||
|
'language.direction',
|
||||||
|
]
|
||||||
|
|
||||||
|
[fr_FR]
|
||||||
|
ignore = [
|
||||||
|
'language.direction',
|
||||||
|
]
|
||||||
|
|
||||||
|
[hi_IN]
|
||||||
|
ignore = [
|
||||||
|
'language.direction',
|
||||||
|
]
|
||||||
|
|
||||||
|
[hu_HU]
|
||||||
|
ignore = [
|
||||||
|
'language.direction',
|
||||||
|
]
|
||||||
|
|
||||||
|
[id_ID]
|
||||||
|
ignore = [
|
||||||
|
'language.direction',
|
||||||
|
]
|
||||||
|
|
||||||
|
[it_IT]
|
||||||
|
ignore = [
|
||||||
|
'font',
|
||||||
|
'language.direction',
|
||||||
|
'no',
|
||||||
|
'password',
|
||||||
|
'pipeline.title',
|
||||||
|
'pipelineOptions.pipelineHeader',
|
||||||
|
'removePassword.selectText.2',
|
||||||
|
'showJS.tags',
|
||||||
|
'sponsor',
|
||||||
|
]
|
||||||
|
|
||||||
|
[ja_JP]
|
||||||
|
ignore = [
|
||||||
|
'language.direction',
|
||||||
|
]
|
||||||
|
|
||||||
|
[ko_KR]
|
||||||
|
ignore = [
|
||||||
|
'language.direction',
|
||||||
|
]
|
||||||
|
|
||||||
|
[nl_NL]
|
||||||
|
ignore = [
|
||||||
|
'language.direction',
|
||||||
|
]
|
||||||
|
|
||||||
|
[pl_PL]
|
||||||
|
ignore = [
|
||||||
|
'language.direction',
|
||||||
|
]
|
||||||
|
|
||||||
|
[pt_BR]
|
||||||
|
ignore = [
|
||||||
|
'language.direction',
|
||||||
|
]
|
||||||
|
|
||||||
|
[pt_PT]
|
||||||
|
ignore = [
|
||||||
|
'language.direction',
|
||||||
|
]
|
||||||
|
|
||||||
|
[ro_RO]
|
||||||
|
ignore = [
|
||||||
|
'language.direction',
|
||||||
|
]
|
||||||
|
|
||||||
|
[ru_RU]
|
||||||
|
ignore = [
|
||||||
|
'language.direction',
|
||||||
|
]
|
||||||
|
|
||||||
|
[sk_SK]
|
||||||
|
ignore = [
|
||||||
|
'language.direction',
|
||||||
|
]
|
||||||
|
|
||||||
|
[sr_LATN_RS]
|
||||||
|
ignore = [
|
||||||
|
'language.direction',
|
||||||
|
]
|
||||||
|
|
||||||
|
[sv_SE]
|
||||||
|
ignore = [
|
||||||
|
'language.direction',
|
||||||
|
]
|
||||||
|
|
||||||
|
[tr_TR]
|
||||||
|
ignore = [
|
||||||
|
'language.direction',
|
||||||
|
]
|
||||||
|
|
||||||
|
[uk_UA]
|
||||||
|
ignore = [
|
||||||
|
'language.direction',
|
||||||
|
]
|
||||||
|
|
||||||
|
[zh_CN]
|
||||||
|
ignore = [
|
||||||
|
'language.direction',
|
||||||
|
]
|
||||||
|
|
||||||
|
[zh_TW]
|
||||||
|
ignore = [
|
||||||
|
'language.direction',
|
||||||
|
]
|
||||||
@@ -6,6 +6,8 @@ import java.net.Socket;
|
|||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
|
import io.github.pixee.security.SystemCommand;
|
||||||
|
|
||||||
public class LibreOfficeListener {
|
public class LibreOfficeListener {
|
||||||
|
|
||||||
private static final long ACTIVITY_TIMEOUT = 20 * 60 * 1000; // 20 minutes
|
private static final long ACTIVITY_TIMEOUT = 20 * 60 * 1000; // 20 minutes
|
||||||
@@ -44,7 +46,7 @@ public class LibreOfficeListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Start the listener process
|
// Start the listener process
|
||||||
process = Runtime.getRuntime().exec("unoconv --listener");
|
process = SystemCommand.runCommand(Runtime.getRuntime(), "unoconv --listener");
|
||||||
lastActivityTime = System.currentTimeMillis();
|
lastActivityTime = System.currentTimeMillis();
|
||||||
|
|
||||||
// Start a background thread to monitor the activity timeout
|
// Start a background thread to monitor the activity timeout
|
||||||
|
|||||||
@@ -1,81 +1,131 @@
|
|||||||
package stirling.software.SPDF;
|
package stirling.software.SPDF;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
import org.springframework.core.env.Environment;
|
import org.springframework.core.env.Environment;
|
||||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||||
|
|
||||||
|
import io.github.pixee.security.SystemCommand;
|
||||||
|
|
||||||
import jakarta.annotation.PostConstruct;
|
import jakarta.annotation.PostConstruct;
|
||||||
import stirling.software.SPDF.config.ConfigInitializer;
|
import stirling.software.SPDF.config.ConfigInitializer;
|
||||||
import stirling.software.SPDF.utils.GeneralUtils;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
|
|
||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
@EnableScheduling
|
@EnableScheduling
|
||||||
public class SPdfApplication {
|
public class SPdfApplication {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(SPdfApplication.class);
|
||||||
|
|
||||||
@Autowired private Environment env;
|
@Autowired private Environment env;
|
||||||
|
|
||||||
|
@Autowired ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
|
private static String serverPortStatic;
|
||||||
|
|
||||||
|
@Value("${server.port:8080}")
|
||||||
|
public void setServerPortStatic(String port) {
|
||||||
|
SPdfApplication.serverPortStatic = port;
|
||||||
|
}
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
public void init() {
|
public void init() {
|
||||||
// Check if the BROWSER_OPEN environment variable is set to true
|
// Check if the BROWSER_OPEN environment variable is set to true
|
||||||
String browserOpenEnv = env.getProperty("BROWSER_OPEN");
|
String browserOpenEnv = env.getProperty("BROWSER_OPEN");
|
||||||
boolean browserOpen = browserOpenEnv != null && browserOpenEnv.equalsIgnoreCase("true");
|
boolean browserOpen = browserOpenEnv != null && "true".equalsIgnoreCase(browserOpenEnv);
|
||||||
|
|
||||||
if (browserOpen) {
|
if (browserOpen) {
|
||||||
try {
|
try {
|
||||||
String url = "http://localhost:" + getPort();
|
String url = "http://localhost:" + getNonStaticPort();
|
||||||
|
|
||||||
String os = System.getProperty("os.name").toLowerCase();
|
String os = System.getProperty("os.name").toLowerCase();
|
||||||
Runtime rt = Runtime.getRuntime();
|
Runtime rt = Runtime.getRuntime();
|
||||||
if (os.contains("win")) {
|
if (os.contains("win")) {
|
||||||
// For Windows
|
// For Windows
|
||||||
rt.exec("rundll32 url.dll,FileProtocolHandler " + url);
|
SystemCommand.runCommand(rt, "rundll32 url.dll,FileProtocolHandler " + url);
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
logger.error("Error opening browser: {}", e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
logger.info("Running configs {}", applicationProperties.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) throws IOException, InterruptedException {
|
||||||
|
|
||||||
SpringApplication app = new SpringApplication(SPdfApplication.class);
|
SpringApplication app = new SpringApplication(SPdfApplication.class);
|
||||||
app.addInitializers(new ConfigInitializer());
|
app.addInitializers(new ConfigInitializer());
|
||||||
|
Map<String, String> propertyFiles = new HashMap<>();
|
||||||
|
|
||||||
|
// stirling pdf settings file
|
||||||
if (Files.exists(Paths.get("configs/settings.yml"))) {
|
if (Files.exists(Paths.get("configs/settings.yml"))) {
|
||||||
app.setDefaultProperties(
|
propertyFiles.put("spring.config.additional-location", "file:configs/settings.yml");
|
||||||
Collections.singletonMap(
|
|
||||||
"spring.config.additional-location", "file:configs/settings.yml"));
|
|
||||||
} else {
|
} else {
|
||||||
System.out.println(
|
logger.warn(
|
||||||
"External configuration file 'configs/settings.yml' does not exist. Using default configuration and environment configuration instead.");
|
"External configuration file 'configs/settings.yml' does not exist. Using default configuration and environment configuration instead.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// custom javs settings file
|
||||||
|
if (Files.exists(Paths.get("configs/custom_settings.yml"))) {
|
||||||
|
String existing = propertyFiles.getOrDefault("spring.config.additional-location", "");
|
||||||
|
if (!existing.isEmpty()) {
|
||||||
|
existing += ",";
|
||||||
|
}
|
||||||
|
propertyFiles.put(
|
||||||
|
"spring.config.additional-location",
|
||||||
|
existing + "file:configs/custom_settings.yml");
|
||||||
|
} else {
|
||||||
|
logger.warn("Custom configuration file 'configs/custom_settings.yml' does not exist.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!propertyFiles.isEmpty()) {
|
||||||
|
app.setDefaultProperties(
|
||||||
|
Collections.singletonMap(
|
||||||
|
"spring.config.additional-location",
|
||||||
|
propertyFiles.get("spring.config.additional-location")));
|
||||||
|
}
|
||||||
|
|
||||||
app.run(args);
|
app.run(args);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Thread.sleep(1000);
|
Thread.sleep(1000);
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
// TODO Auto-generated catch block
|
Thread.currentThread().interrupt();
|
||||||
e.printStackTrace();
|
throw new RuntimeException("Thread interrupted while sleeping", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
GeneralUtils.createDir("customFiles/static/");
|
try {
|
||||||
GeneralUtils.createDir("customFiles/templates/");
|
Files.createDirectories(Path.of("customFiles/static/"));
|
||||||
|
Files.createDirectories(Path.of("customFiles/templates/"));
|
||||||
System.out.println("Stirling-PDF Started.");
|
} catch (Exception e) {
|
||||||
|
logger.error("Error creating directories: {}", e.getMessage());
|
||||||
String url = "http://localhost:" + getPort();
|
}
|
||||||
System.out.println("Navigate to " + url);
|
printStartupLogs();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getPort() {
|
private static void printStartupLogs() {
|
||||||
String port = System.getProperty("local.server.port");
|
logger.info("Stirling-PDF Started.");
|
||||||
if (port == null || port.isEmpty()) {
|
String url = "http://localhost:" + getStaticPort();
|
||||||
port = "8080";
|
logger.info("Navigate to {}", url);
|
||||||
}
|
}
|
||||||
return port;
|
|
||||||
|
public static String getStaticPort() {
|
||||||
|
return serverPortStatic;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getNonStaticPort() {
|
||||||
|
return serverPortStatic;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,18 +6,35 @@ import java.nio.file.Paths;
|
|||||||
import java.util.Properties;
|
import java.util.Properties;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
import org.springframework.core.io.ClassPathResource;
|
import org.springframework.core.io.ClassPathResource;
|
||||||
import org.springframework.core.io.Resource;
|
import org.springframework.core.io.Resource;
|
||||||
|
import org.springframework.core.io.ResourceLoader;
|
||||||
|
import org.thymeleaf.spring6.SpringTemplateEngine;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
|
@Lazy
|
||||||
public class AppConfig {
|
public class AppConfig {
|
||||||
|
|
||||||
@Autowired ApplicationProperties applicationProperties;
|
@Autowired ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnProperty(
|
||||||
|
name = "system.customHTMLFiles",
|
||||||
|
havingValue = "true",
|
||||||
|
matchIfMissing = false)
|
||||||
|
public SpringTemplateEngine templateEngine(ResourceLoader resourceLoader) {
|
||||||
|
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
|
||||||
|
templateEngine.addTemplateResolver(new FileFallbackTemplateResolver(resourceLoader));
|
||||||
|
return templateEngine;
|
||||||
|
}
|
||||||
|
|
||||||
@Bean(name = "loginEnabled")
|
@Bean(name = "loginEnabled")
|
||||||
public boolean loginEnabled() {
|
public boolean loginEnabled() {
|
||||||
return applicationProperties.getSecurity().getEnableLogin();
|
return applicationProperties.getSecurity().getEnableLogin();
|
||||||
@@ -77,16 +94,18 @@ public class AppConfig {
|
|||||||
return Files.exists(Paths.get("/.dockerenv"));
|
return Files.exists(Paths.get("/.dockerenv"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean(name = "bookFormatsInstalled")
|
@Bean(name = "bookAndHtmlFormatsInstalled")
|
||||||
public boolean bookFormatsInstalled() {
|
public boolean bookAndHtmlFormatsInstalled() {
|
||||||
return applicationProperties.getSystem().getCustomApplications().isInstallBookFormats();
|
String installOps = System.getProperty("INSTALL_BOOK_AND_ADVANCED_HTML_OPS");
|
||||||
|
if (installOps == null) {
|
||||||
|
installOps = System.getenv("INSTALL_BOOK_AND_ADVANCED_HTML_OPS");
|
||||||
|
}
|
||||||
|
return "true".equalsIgnoreCase(installOps);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean(name = "htmlFormatsInstalled")
|
@ConditionalOnMissingClass("stirling.software.SPDF.config.security.SecurityConfiguration")
|
||||||
public boolean htmlFormatsInstalled() {
|
@Bean(name = "activSecurity")
|
||||||
return applicationProperties
|
public boolean missingActivSecurity() {
|
||||||
.getSystem()
|
return false;
|
||||||
.getCustomApplications()
|
|
||||||
.isInstallAdvancedHtmlToPDF();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package stirling.software.SPDF.config;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Scope;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class AppUpdateService {
|
||||||
|
|
||||||
|
@Autowired private ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
|
@Autowired(required = false)
|
||||||
|
ShowAdminInterface showAdmin;
|
||||||
|
|
||||||
|
@Bean(name = "shouldShow")
|
||||||
|
@Scope("request")
|
||||||
|
public boolean shouldShow() {
|
||||||
|
boolean showUpdate = applicationProperties.getSystem().getShowUpdate();
|
||||||
|
boolean showAdminResult = (showAdmin != null) ? showAdmin.getShowUpdateOnlyAdmins() : true;
|
||||||
|
return showUpdate && showAdminResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,7 +15,14 @@ public class CleanUrlInterceptor implements HandlerInterceptor {
|
|||||||
|
|
||||||
private static final List<String> ALLOWED_PARAMS =
|
private static final List<String> ALLOWED_PARAMS =
|
||||||
Arrays.asList(
|
Arrays.asList(
|
||||||
"lang", "endpoint", "endpoints", "logout", "error", "file", "messageType");
|
"lang",
|
||||||
|
"endpoint",
|
||||||
|
"endpoints",
|
||||||
|
"logout",
|
||||||
|
"error",
|
||||||
|
"erroroauth",
|
||||||
|
"file",
|
||||||
|
"messageType");
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean preHandle(
|
public boolean preHandle(
|
||||||
|
|||||||
@@ -1,20 +1,18 @@
|
|||||||
package stirling.software.SPDF.config;
|
package stirling.software.SPDF.config;
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.InputStreamReader;
|
import java.net.URISyntaxException;
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.function.Function;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import org.springframework.context.ApplicationContextInitializer;
|
import org.springframework.context.ApplicationContextInitializer;
|
||||||
import org.springframework.context.ConfigurableApplicationContext;
|
import org.springframework.context.ConfigurableApplicationContext;
|
||||||
@@ -26,12 +24,12 @@ public class ConfigInitializer
|
|||||||
public void initialize(ConfigurableApplicationContext applicationContext) {
|
public void initialize(ConfigurableApplicationContext applicationContext) {
|
||||||
try {
|
try {
|
||||||
ensureConfigExists();
|
ensureConfigExists();
|
||||||
} catch (IOException e) {
|
} catch (Exception e) {
|
||||||
throw new RuntimeException("Failed to initialize application configuration", e);
|
throw new RuntimeException("Failed to initialize application configuration", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ensureConfigExists() throws IOException {
|
public void ensureConfigExists() throws IOException, URISyntaxException {
|
||||||
// Define the path to the external config directory
|
// Define the path to the external config directory
|
||||||
Path destPath = Paths.get("configs", "settings.yml");
|
Path destPath = Paths.get("configs", "settings.yml");
|
||||||
|
|
||||||
@@ -51,93 +49,90 @@ public class ConfigInitializer
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// If user file exists, we need to merge it with the template from the classpath
|
Path templatePath =
|
||||||
List<String> templateLines;
|
Paths.get(
|
||||||
try (InputStream in =
|
getClass()
|
||||||
getClass().getClassLoader().getResourceAsStream("settings.yml.template")) {
|
.getClassLoader()
|
||||||
templateLines =
|
.getResource("settings.yml.template")
|
||||||
new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))
|
.toURI());
|
||||||
.lines()
|
Path userPath = Paths.get("configs", "settings.yml");
|
||||||
.collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
|
|
||||||
mergeYamlFiles(templateLines, destPath, destPath);
|
List<String> templateLines = Files.readAllLines(templatePath);
|
||||||
|
List<String> userLines =
|
||||||
|
Files.exists(userPath) ? Files.readAllLines(userPath) : new ArrayList<>();
|
||||||
|
|
||||||
|
List<String> resultLines = new ArrayList<>();
|
||||||
|
|
||||||
|
for (String templateLine : templateLines) {
|
||||||
|
// Check if the line is a comment
|
||||||
|
if (templateLine.trim().startsWith("#")) {
|
||||||
|
String entry = templateLine.trim().substring(1).trim();
|
||||||
|
if (!entry.isEmpty()) {
|
||||||
|
// Check if this comment has been uncommented in userLines
|
||||||
|
String key = entry.split(":")[0].trim();
|
||||||
|
System.out.println("key=" + key + ", entry=" + entry );
|
||||||
|
addLine(resultLines, userLines, templateLine, key);
|
||||||
|
} else {
|
||||||
|
resultLines.add(templateLine);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Check if the line is a key-value pair
|
||||||
|
else if (templateLine.contains(":")) {
|
||||||
|
String key = templateLine.split(":")[0].trim();
|
||||||
|
addLine(resultLines, userLines, templateLine, key);
|
||||||
|
}
|
||||||
|
// Handle empty lines
|
||||||
|
else if (templateLine.trim().length() == 0) {
|
||||||
|
resultLines.add("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the result to the user settings file
|
||||||
|
Files.write(userPath, resultLines);
|
||||||
|
}
|
||||||
|
|
||||||
|
Path customSettingsPath = Paths.get("configs", "custom_settings.yml");
|
||||||
|
if (!Files.exists(customSettingsPath)) {
|
||||||
|
Files.createFile(customSettingsPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void mergeYamlFiles(List<String> templateLines, Path userFilePath, Path outputPath)
|
|
||||||
throws IOException {
|
|
||||||
List<String> userLines = Files.readAllLines(userFilePath);
|
//TODO check parent value instead of just indent lines for duplicate keys (like enabled etc)
|
||||||
List<String> mergedLines = new ArrayList<>();
|
private static void addLine(List<String> resultLines, List<String> userLines, String templateLine, String key) {
|
||||||
boolean insideAutoGenerated = false;
|
boolean added = false;
|
||||||
boolean beforeFirstKey = true;
|
int templateIndentationLevel = getIndentationLevel(templateLine);
|
||||||
|
for (String settingsLine : userLines) {
|
||||||
Function<String, Boolean> isCommented = line -> line.trim().startsWith("#");
|
if(settingsLine.contains("oauth2") || settingsLine.contains("enabled") )
|
||||||
Function<String, String> extractKey =
|
if (settingsLine.trim().startsWith(key + ":")) {
|
||||||
line -> {
|
int settingsIndentationLevel = getIndentationLevel(settingsLine);
|
||||||
String[] parts = line.split(":");
|
// Check if it is correct settingsLine and has the same parent as templateLine
|
||||||
return parts.length > 0 ? parts[0].trim().replace("#", "").trim() : "";
|
if (settingsIndentationLevel == templateIndentationLevel) {
|
||||||
};
|
resultLines.add(settingsLine);
|
||||||
|
added = true;
|
||||||
Set<String> userKeys = userLines.stream().map(extractKey).collect(Collectors.toSet());
|
break;
|
||||||
|
}
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!added) {
|
||||||
|
resultLines.add(templateLine);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Add any additional uncommented user lines that are not present in the
|
private static int getIndentationLevel(String line) {
|
||||||
// template
|
int indentationLevel = 0;
|
||||||
for (String userLine : userLines) {
|
String trimmedLine = line.trim();
|
||||||
String userKey = extractKey.apply(userLine);
|
if (trimmedLine.startsWith("#")) {
|
||||||
boolean isPresentInTemplate =
|
line = trimmedLine.substring(1);
|
||||||
templateLines.stream()
|
}
|
||||||
.map(extractKey)
|
for (char c : line.toCharArray()) {
|
||||||
.anyMatch(templateKey -> templateKey.equalsIgnoreCase(userKey));
|
if (c == ' ') {
|
||||||
if (!isPresentInTemplate && !isCommented.apply(userLine)) {
|
indentationLevel++;
|
||||||
mergedLines.add(userLine);
|
} else {
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return indentationLevel;
|
||||||
Files.write(outputPath, mergedLines, StandardCharsets.UTF_8);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import org.springframework.stereotype.Service;
|
|||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@DependsOn({"bookFormatsInstalled"})
|
@DependsOn({"bookAndHtmlFormatsInstalled"})
|
||||||
public class EndpointConfiguration {
|
public class EndpointConfiguration {
|
||||||
private static final Logger logger = LoggerFactory.getLogger(EndpointConfiguration.class);
|
private static final Logger logger = LoggerFactory.getLogger(EndpointConfiguration.class);
|
||||||
private Map<String, Boolean> endpointStatuses = new ConcurrentHashMap<>();
|
private Map<String, Boolean> endpointStatuses = new ConcurrentHashMap<>();
|
||||||
@@ -24,14 +24,14 @@ public class EndpointConfiguration {
|
|||||||
|
|
||||||
private final ApplicationProperties applicationProperties;
|
private final ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
private boolean bookFormatsInstalled;
|
private boolean bookAndHtmlFormatsInstalled;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public EndpointConfiguration(
|
public EndpointConfiguration(
|
||||||
ApplicationProperties applicationProperties,
|
ApplicationProperties applicationProperties,
|
||||||
@Qualifier("bookFormatsInstalled") boolean bookFormatsInstalled) {
|
@Qualifier("bookAndHtmlFormatsInstalled") boolean bookAndHtmlFormatsInstalled) {
|
||||||
this.applicationProperties = applicationProperties;
|
this.applicationProperties = applicationProperties;
|
||||||
this.bookFormatsInstalled = bookFormatsInstalled;
|
this.bookAndHtmlFormatsInstalled = bookAndHtmlFormatsInstalled;
|
||||||
init();
|
init();
|
||||||
processEnvironmentConfigs();
|
processEnvironmentConfigs();
|
||||||
}
|
}
|
||||||
@@ -129,7 +129,7 @@ public class EndpointConfiguration {
|
|||||||
addEndpointToGroup("Other", "sign");
|
addEndpointToGroup("Other", "sign");
|
||||||
addEndpointToGroup("Other", "flatten");
|
addEndpointToGroup("Other", "flatten");
|
||||||
addEndpointToGroup("Other", "repair");
|
addEndpointToGroup("Other", "repair");
|
||||||
addEndpointToGroup("Other", "remove-blanks");
|
addEndpointToGroup("Other", REMOVE_BLANKS);
|
||||||
addEndpointToGroup("Other", "remove-annotations");
|
addEndpointToGroup("Other", "remove-annotations");
|
||||||
addEndpointToGroup("Other", "compare");
|
addEndpointToGroup("Other", "compare");
|
||||||
addEndpointToGroup("Other", "add-page-numbers");
|
addEndpointToGroup("Other", "add-page-numbers");
|
||||||
@@ -140,14 +140,12 @@ public class EndpointConfiguration {
|
|||||||
// CLI
|
// CLI
|
||||||
addEndpointToGroup("CLI", "compress-pdf");
|
addEndpointToGroup("CLI", "compress-pdf");
|
||||||
addEndpointToGroup("CLI", "extract-image-scans");
|
addEndpointToGroup("CLI", "extract-image-scans");
|
||||||
addEndpointToGroup("CLI", "remove-blanks");
|
|
||||||
addEndpointToGroup("CLI", "repair");
|
addEndpointToGroup("CLI", "repair");
|
||||||
addEndpointToGroup("CLI", "pdf-to-pdfa");
|
addEndpointToGroup("CLI", "pdf-to-pdfa");
|
||||||
addEndpointToGroup("CLI", "file-to-pdf");
|
addEndpointToGroup("CLI", "file-to-pdf");
|
||||||
addEndpointToGroup("CLI", "xlsx-to-pdf");
|
addEndpointToGroup("CLI", "xlsx-to-pdf");
|
||||||
addEndpointToGroup("CLI", "pdf-to-word");
|
addEndpointToGroup("CLI", "pdf-to-word");
|
||||||
addEndpointToGroup("CLI", "pdf-to-presentation");
|
addEndpointToGroup("CLI", "pdf-to-presentation");
|
||||||
addEndpointToGroup("CLI", "pdf-to-text");
|
|
||||||
addEndpointToGroup("CLI", "pdf-to-html");
|
addEndpointToGroup("CLI", "pdf-to-html");
|
||||||
addEndpointToGroup("CLI", "pdf-to-xml");
|
addEndpointToGroup("CLI", "pdf-to-xml");
|
||||||
addEndpointToGroup("CLI", "ocr-pdf");
|
addEndpointToGroup("CLI", "ocr-pdf");
|
||||||
@@ -155,6 +153,7 @@ public class EndpointConfiguration {
|
|||||||
addEndpointToGroup("CLI", "url-to-pdf");
|
addEndpointToGroup("CLI", "url-to-pdf");
|
||||||
addEndpointToGroup("CLI", "book-to-pdf");
|
addEndpointToGroup("CLI", "book-to-pdf");
|
||||||
addEndpointToGroup("CLI", "pdf-to-book");
|
addEndpointToGroup("CLI", "pdf-to-book");
|
||||||
|
addEndpointToGroup("CLI", "pdf-to-rtf");
|
||||||
|
|
||||||
// Calibre
|
// Calibre
|
||||||
addEndpointToGroup("Calibre", "book-to-pdf");
|
addEndpointToGroup("Calibre", "book-to-pdf");
|
||||||
@@ -162,13 +161,13 @@ public class EndpointConfiguration {
|
|||||||
|
|
||||||
// python
|
// python
|
||||||
addEndpointToGroup("Python", "extract-image-scans");
|
addEndpointToGroup("Python", "extract-image-scans");
|
||||||
addEndpointToGroup("Python", "remove-blanks");
|
addEndpointToGroup("Python", REMOVE_BLANKS);
|
||||||
addEndpointToGroup("Python", "html-to-pdf");
|
addEndpointToGroup("Python", "html-to-pdf");
|
||||||
addEndpointToGroup("Python", "url-to-pdf");
|
addEndpointToGroup("Python", "url-to-pdf");
|
||||||
|
|
||||||
// openCV
|
// openCV
|
||||||
addEndpointToGroup("OpenCV", "extract-image-scans");
|
addEndpointToGroup("OpenCV", "extract-image-scans");
|
||||||
addEndpointToGroup("OpenCV", "remove-blanks");
|
addEndpointToGroup("OpenCV", REMOVE_BLANKS);
|
||||||
|
|
||||||
// LibreOffice
|
// LibreOffice
|
||||||
addEndpointToGroup("LibreOffice", "repair");
|
addEndpointToGroup("LibreOffice", "repair");
|
||||||
@@ -176,7 +175,7 @@ public class EndpointConfiguration {
|
|||||||
addEndpointToGroup("LibreOffice", "xlsx-to-pdf");
|
addEndpointToGroup("LibreOffice", "xlsx-to-pdf");
|
||||||
addEndpointToGroup("LibreOffice", "pdf-to-word");
|
addEndpointToGroup("LibreOffice", "pdf-to-word");
|
||||||
addEndpointToGroup("LibreOffice", "pdf-to-presentation");
|
addEndpointToGroup("LibreOffice", "pdf-to-presentation");
|
||||||
addEndpointToGroup("LibreOffice", "pdf-to-text");
|
addEndpointToGroup("LibreOffice", "pdf-to-rtf");
|
||||||
addEndpointToGroup("LibreOffice", "pdf-to-html");
|
addEndpointToGroup("LibreOffice", "pdf-to-html");
|
||||||
addEndpointToGroup("LibreOffice", "pdf-to-xml");
|
addEndpointToGroup("LibreOffice", "pdf-to-xml");
|
||||||
|
|
||||||
@@ -218,6 +217,8 @@ public class EndpointConfiguration {
|
|||||||
addEndpointToGroup("Java", "split-by-size-or-count");
|
addEndpointToGroup("Java", "split-by-size-or-count");
|
||||||
addEndpointToGroup("Java", "overlay-pdf");
|
addEndpointToGroup("Java", "overlay-pdf");
|
||||||
addEndpointToGroup("Java", "split-pdf-by-sections");
|
addEndpointToGroup("Java", "split-pdf-by-sections");
|
||||||
|
addEndpointToGroup("Java", REMOVE_BLANKS);
|
||||||
|
addEndpointToGroup("Java", "pdf-to-text");
|
||||||
|
|
||||||
// Javascript
|
// Javascript
|
||||||
addEndpointToGroup("Javascript", "pdf-organizer");
|
addEndpointToGroup("Javascript", "pdf-organizer");
|
||||||
@@ -229,7 +230,7 @@ public class EndpointConfiguration {
|
|||||||
private void processEnvironmentConfigs() {
|
private void processEnvironmentConfigs() {
|
||||||
List<String> endpointsToRemove = applicationProperties.getEndpoints().getToRemove();
|
List<String> endpointsToRemove = applicationProperties.getEndpoints().getToRemove();
|
||||||
List<String> groupsToRemove = applicationProperties.getEndpoints().getGroupsToRemove();
|
List<String> groupsToRemove = applicationProperties.getEndpoints().getGroupsToRemove();
|
||||||
if (!bookFormatsInstalled) {
|
if (!bookAndHtmlFormatsInstalled) {
|
||||||
groupsToRemove.add("Calibre");
|
groupsToRemove.add("Calibre");
|
||||||
}
|
}
|
||||||
if (endpointsToRemove != null) {
|
if (endpointsToRemove != null) {
|
||||||
@@ -244,4 +245,6 @@ public class EndpointConfiguration {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final String REMOVE_BLANKS = "remove-blanks";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package stirling.software.SPDF.config;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.springframework.core.io.Resource;
|
||||||
|
import org.springframework.core.io.ResourceLoader;
|
||||||
|
import org.thymeleaf.IEngineConfiguration;
|
||||||
|
import org.thymeleaf.templateresolver.AbstractConfigurableTemplateResolver;
|
||||||
|
import org.thymeleaf.templateresource.ClassLoaderTemplateResource;
|
||||||
|
import org.thymeleaf.templateresource.FileTemplateResource;
|
||||||
|
import org.thymeleaf.templateresource.ITemplateResource;
|
||||||
|
|
||||||
|
public class FileFallbackTemplateResolver extends AbstractConfigurableTemplateResolver {
|
||||||
|
|
||||||
|
private final ResourceLoader resourceLoader;
|
||||||
|
|
||||||
|
public FileFallbackTemplateResolver(ResourceLoader resourceLoader) {
|
||||||
|
super();
|
||||||
|
this.resourceLoader = resourceLoader;
|
||||||
|
setSuffix(".html");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note this does not work in local IDE, Prod jar only.
|
||||||
|
@Override
|
||||||
|
protected ITemplateResource computeTemplateResource(
|
||||||
|
IEngineConfiguration configuration,
|
||||||
|
String ownerTemplate,
|
||||||
|
String template,
|
||||||
|
String resourceName,
|
||||||
|
String characterEncoding,
|
||||||
|
Map<String, Object> templateResolutionAttributes) {
|
||||||
|
Resource resource =
|
||||||
|
resourceLoader.getResource("file:./customFiles/templates/" + resourceName);
|
||||||
|
try {
|
||||||
|
if (resource.exists() && resource.isReadable()) {
|
||||||
|
return new FileTemplateResource(resource.getFile().getPath(), characterEncoding);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ClassLoaderTemplateResource(
|
||||||
|
Thread.currentThread().getContextClassLoader(),
|
||||||
|
"classpath:/templates/" + resourceName,
|
||||||
|
characterEncoding);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
package stirling.software.SPDF.config;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import jakarta.annotation.PostConstruct;
|
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
|
||||||
import stirling.software.SPDF.utils.ProcessExecutor;
|
|
||||||
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
|
|
||||||
|
|
||||||
@Component
|
|
||||||
public class PostStartupProcesses {
|
|
||||||
|
|
||||||
@Autowired ApplicationProperties applicationProperties;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
@Qualifier("RunningInDocker")
|
|
||||||
private boolean runningInDocker;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
@Qualifier("bookFormatsInstalled")
|
|
||||||
private boolean bookFormatsInstalled;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
@Qualifier("htmlFormatsInstalled")
|
|
||||||
private boolean htmlFormatsInstalled;
|
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(PostStartupProcesses.class);
|
|
||||||
|
|
||||||
@PostConstruct
|
|
||||||
public void runInstallCommandBasedOnEnvironment() throws IOException, InterruptedException {
|
|
||||||
List<List<String>> commands = new ArrayList<>();
|
|
||||||
// Checking for DOCKER_INSTALL_BOOK_FORMATS environment variable
|
|
||||||
if (bookFormatsInstalled) {
|
|
||||||
List<String> tmpList = new ArrayList<>();
|
|
||||||
// Set up the timezone configuration commands
|
|
||||||
tmpList.addAll(
|
|
||||||
Arrays.asList(
|
|
||||||
"sh",
|
|
||||||
"-c",
|
|
||||||
"echo 'tzdata tzdata/Areas select Europe' | debconf-set-selections; "
|
|
||||||
+ "echo 'tzdata tzdata/Zones/Europe select Berlin' | debconf-set-selections"));
|
|
||||||
commands.add(tmpList);
|
|
||||||
|
|
||||||
// Install calibre with DEBIAN_FRONTEND set to noninteractive
|
|
||||||
tmpList = new ArrayList<>();
|
|
||||||
tmpList.addAll(
|
|
||||||
Arrays.asList(
|
|
||||||
"sh",
|
|
||||||
"-c",
|
|
||||||
"DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends calibre"));
|
|
||||||
commands.add(tmpList);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Checking for DOCKER_INSTALL_HTML_FORMATS environment variable
|
|
||||||
if (htmlFormatsInstalled) {
|
|
||||||
List<String> tmpList = new ArrayList<>();
|
|
||||||
// Add -y flag for automatic yes to prompts and --no-install-recommends to reduce size
|
|
||||||
tmpList.addAll(
|
|
||||||
Arrays.asList(
|
|
||||||
"apt-get", "install", "wkhtmltopdf", "-y", "--no-install-recommends"));
|
|
||||||
commands.add(tmpList);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!commands.isEmpty()) {
|
|
||||||
// Run the command
|
|
||||||
if (runningInDocker) {
|
|
||||||
List<String> tmpList = new ArrayList<>();
|
|
||||||
tmpList.addAll(Arrays.asList("apt-get", "update"));
|
|
||||||
commands.add(0, tmpList);
|
|
||||||
|
|
||||||
for (List<String> list : commands) {
|
|
||||||
ProcessExecutorResult returnCode =
|
|
||||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.INSTALL_APP, true)
|
|
||||||
.runCommandWithOutputHandling(list);
|
|
||||||
logger.info("RC for app installs {}", returnCode.getRc());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
"Not running inside Docker so skipping automated install process with command.");
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
if (runningInDocker) {
|
|
||||||
logger.info("No custom apps to install.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package stirling.software.SPDF.config;
|
||||||
|
|
||||||
|
public interface ShowAdminInterface {
|
||||||
|
default boolean getShowUpdateOnlyAdmins() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package stirling.software.SPDF.config.security;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.config.ShowAdminInterface;
|
||||||
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
|
import stirling.software.SPDF.model.User;
|
||||||
|
import stirling.software.SPDF.repository.UserRepository;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class AppUpdateAuthService implements ShowAdminInterface {
|
||||||
|
|
||||||
|
@Autowired private UserRepository userRepository;
|
||||||
|
@Autowired private ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
|
public boolean getShowUpdateOnlyAdmins() {
|
||||||
|
boolean showUpdate = applicationProperties.getSystem().getShowUpdate();
|
||||||
|
if (!showUpdate) {
|
||||||
|
return showUpdate;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean showUpdateOnlyAdmin = applicationProperties.getSystem().getShowUpdateOnlyAdmin();
|
||||||
|
|
||||||
|
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||||
|
|
||||||
|
if (authentication == null || !authentication.isAuthenticated()) {
|
||||||
|
return !showUpdateOnlyAdmin;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authentication.getName().equalsIgnoreCase("anonymousUser")) {
|
||||||
|
return !showUpdateOnlyAdmin;
|
||||||
|
}
|
||||||
|
|
||||||
|
Optional<User> user = userRepository.findByUsername(authentication.getName());
|
||||||
|
if (user.isPresent() && showUpdateOnlyAdmin) {
|
||||||
|
return "ROLE_ADMIN".equals(user.get().getRolesAsString());
|
||||||
|
}
|
||||||
|
|
||||||
|
return showUpdate;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,26 +1,35 @@
|
|||||||
package stirling.software.SPDF.config.security;
|
package stirling.software.SPDF.config.security;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.security.authentication.BadCredentialsException;
|
import org.springframework.security.authentication.BadCredentialsException;
|
||||||
|
import org.springframework.security.authentication.InternalAuthenticationServiceException;
|
||||||
import org.springframework.security.authentication.LockedException;
|
import org.springframework.security.authentication.LockedException;
|
||||||
import org.springframework.security.core.AuthenticationException;
|
import org.springframework.security.core.AuthenticationException;
|
||||||
|
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||||
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
|
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import jakarta.servlet.ServletException;
|
import jakarta.servlet.ServletException;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import stirling.software.SPDF.model.User;
|
||||||
|
|
||||||
@Component
|
|
||||||
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
|
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
|
||||||
|
|
||||||
@Autowired private final LoginAttemptService loginAttemptService;
|
private LoginAttemptService loginAttemptService;
|
||||||
|
|
||||||
@Autowired
|
private UserService userService;
|
||||||
public CustomAuthenticationFailureHandler(LoginAttemptService loginAttemptService) {
|
|
||||||
|
private static final Logger logger =
|
||||||
|
LoggerFactory.getLogger(CustomAuthenticationFailureHandler.class);
|
||||||
|
|
||||||
|
public CustomAuthenticationFailureHandler(
|
||||||
|
final LoginAttemptService loginAttemptService, UserService userService) {
|
||||||
this.loginAttemptService = loginAttemptService;
|
this.loginAttemptService = loginAttemptService;
|
||||||
|
this.userService = userService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -29,21 +38,42 @@ public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationF
|
|||||||
HttpServletResponse response,
|
HttpServletResponse response,
|
||||||
AuthenticationException exception)
|
AuthenticationException exception)
|
||||||
throws IOException, ServletException {
|
throws IOException, ServletException {
|
||||||
|
|
||||||
String ip = request.getRemoteAddr();
|
String ip = request.getRemoteAddr();
|
||||||
logger.error("Failed login attempt from IP: " + ip);
|
logger.error("Failed login attempt from IP: {}", ip);
|
||||||
|
|
||||||
|
if (exception.getClass().isAssignableFrom(InternalAuthenticationServiceException.class)
|
||||||
|
|| "Password must not be null".equalsIgnoreCase(exception.getMessage())) {
|
||||||
|
response.sendRedirect("/login?error=oauth2AuthenticationError");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
String username = request.getParameter("username");
|
String username = request.getParameter("username");
|
||||||
if (loginAttemptService.loginAttemptCheck(username)) {
|
if (username != null && !isDemoUser(username)) {
|
||||||
setDefaultFailureUrl("/login?error=locked");
|
logger.info(
|
||||||
|
"Remaining attempts for user {}: {}",
|
||||||
} else {
|
username,
|
||||||
if (exception.getClass().isAssignableFrom(BadCredentialsException.class)) {
|
loginAttemptService.getRemainingAttempts(username));
|
||||||
setDefaultFailureUrl("/login?error=badcredentials");
|
loginAttemptService.loginFailed(username);
|
||||||
} else if (exception.getClass().isAssignableFrom(LockedException.class)) {
|
if (loginAttemptService.isBlocked(username)
|
||||||
setDefaultFailureUrl("/login?error=locked");
|
|| exception.getClass().isAssignableFrom(LockedException.class)) {
|
||||||
|
response.sendRedirect("/login?error=locked");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (exception.getClass().isAssignableFrom(BadCredentialsException.class)
|
||||||
|
|| exception.getClass().isAssignableFrom(UsernameNotFoundException.class)) {
|
||||||
|
response.sendRedirect("/login?error=badcredentials");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
super.onAuthenticationFailure(request, response, exception);
|
super.onAuthenticationFailure(request, response, exception);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isDemoUser(String username) {
|
||||||
|
Optional<User> user = userService.findByUsernameIgnoreCase(username);
|
||||||
|
return user.isPresent()
|
||||||
|
&& user.get().getAuthorities().stream()
|
||||||
|
.anyMatch(authority -> "ROLE_DEMO_USER".equals(authority.getAuthority()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,9 @@ package stirling.software.SPDF.config.security;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
|
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
|
||||||
import org.springframework.security.web.savedrequest.SavedRequest;
|
import org.springframework.security.web.savedrequest.SavedRequest;
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import jakarta.servlet.ServletException;
|
import jakarta.servlet.ServletException;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
@@ -14,25 +12,30 @@ import jakarta.servlet.http.HttpServletResponse;
|
|||||||
import jakarta.servlet.http.HttpSession;
|
import jakarta.servlet.http.HttpSession;
|
||||||
import stirling.software.SPDF.utils.RequestUriUtils;
|
import stirling.software.SPDF.utils.RequestUriUtils;
|
||||||
|
|
||||||
@Component
|
|
||||||
public class CustomAuthenticationSuccessHandler
|
public class CustomAuthenticationSuccessHandler
|
||||||
extends SavedRequestAwareAuthenticationSuccessHandler {
|
extends SavedRequestAwareAuthenticationSuccessHandler {
|
||||||
|
|
||||||
@Autowired private LoginAttemptService loginAttemptService;
|
private LoginAttemptService loginAttemptService;
|
||||||
|
|
||||||
|
public CustomAuthenticationSuccessHandler(LoginAttemptService loginAttemptService) {
|
||||||
|
this.loginAttemptService = loginAttemptService;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAuthenticationSuccess(
|
public void onAuthenticationSuccess(
|
||||||
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
|
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
|
||||||
throws ServletException, IOException {
|
throws ServletException, IOException {
|
||||||
String username = request.getParameter("username");
|
|
||||||
loginAttemptService.loginSucceeded(username);
|
String userName = request.getParameter("username");
|
||||||
|
loginAttemptService.loginSucceeded(userName);
|
||||||
|
|
||||||
// Get the saved request
|
// Get the saved request
|
||||||
HttpSession session = request.getSession(false);
|
HttpSession session = request.getSession(false);
|
||||||
SavedRequest savedRequest =
|
SavedRequest savedRequest =
|
||||||
session != null
|
(session != null)
|
||||||
? (SavedRequest) session.getAttribute("SPRING_SECURITY_SAVED_REQUEST")
|
? (SavedRequest) session.getAttribute("SPRING_SECURITY_SAVED_REQUEST")
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
if (savedRequest != null
|
if (savedRequest != null
|
||||||
&& !RequestUriUtils.isStaticResource(savedRequest.getRedirectUrl())) {
|
&& !RequestUriUtils.isStaticResource(savedRequest.getRedirectUrl())) {
|
||||||
// Redirect to the original destination
|
// Redirect to the original destination
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
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.core.session.SessionRegistry;
|
||||||
|
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
|
||||||
|
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import jakarta.servlet.http.HttpSession;
|
||||||
|
|
||||||
|
public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
|
||||||
|
|
||||||
|
@Autowired SessionRegistry sessionRegistry;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onLogoutSuccess(
|
||||||
|
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
|
||||||
|
throws IOException, ServletException {
|
||||||
|
HttpSession session = request.getSession(false);
|
||||||
|
if (session != null) {
|
||||||
|
String sessionId = session.getId();
|
||||||
|
sessionRegistry.removeSessionInformation(sessionId);
|
||||||
|
session.invalidate();
|
||||||
|
logger.debug("Session invalidated: " + sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
response.sendRedirect(request.getContextPath() + "/login?logout=true");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -39,6 +39,10 @@ public class CustomUserDetailsService implements UserDetailsService {
|
|||||||
"Your account has been locked due to too many failed login attempts.");
|
"Your account has been locked due to too many failed login attempts.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!user.hasPassword()) {
|
||||||
|
throw new IllegalArgumentException("Password must not be null");
|
||||||
|
}
|
||||||
|
|
||||||
return new org.springframework.security.core.userdetails.User(
|
return new org.springframework.security.core.userdetails.User(
|
||||||
user.getUsername(),
|
user.getUsername(),
|
||||||
user.getPassword(),
|
user.getPassword(),
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ public class FirstLoginFilter extends OncePerRequestFilter {
|
|||||||
|
|
||||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||||
if (authentication != null && authentication.isAuthenticated()) {
|
if (authentication != null && authentication.isAuthenticated()) {
|
||||||
Optional<User> user = userService.findByUsername(authentication.getName());
|
Optional<User> user = userService.findByUsernameIgnoreCase(authentication.getName());
|
||||||
if ("GET".equalsIgnoreCase(method)
|
if ("GET".equalsIgnoreCase(method)
|
||||||
&& user.isPresent()
|
&& user.isPresent()
|
||||||
&& user.get().isFirstLogin()
|
&& user.get().isFirstLogin()
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import java.nio.file.Paths;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@@ -19,43 +21,70 @@ public class InitialSecuritySetup {
|
|||||||
|
|
||||||
@Autowired private UserService userService;
|
@Autowired private UserService userService;
|
||||||
|
|
||||||
@Autowired ApplicationProperties applicationProperties;
|
@Autowired private ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(InitialSecuritySetup.class);
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
public void init() {
|
public void init() {
|
||||||
if (!userService.hasUsers()) {
|
if (!userService.hasUsers()) {
|
||||||
|
initializeAdminUser();
|
||||||
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());
|
|
||||||
}
|
}
|
||||||
|
initializeInternalApiUser();
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
public void initSecretKey() throws IOException {
|
public void initSecretKey() throws IOException {
|
||||||
String secretKey = applicationProperties.getAutomaticallyGenerated().getKey();
|
String secretKey = applicationProperties.getAutomaticallyGenerated().getKey();
|
||||||
if (secretKey == null || secretKey.isEmpty()) {
|
if (!isValidUUID(secretKey)) {
|
||||||
secretKey = UUID.randomUUID().toString(); // Generating a random UUID as the secret key
|
secretKey = UUID.randomUUID().toString(); // Generating a random UUID as the secret key
|
||||||
saveKeyToConfig(secretKey);
|
saveKeyToConfig(secretKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void initializeAdminUser() {
|
||||||
|
String initialUsername =
|
||||||
|
applicationProperties.getSecurity().getInitialLogin().getUsername();
|
||||||
|
String initialPassword =
|
||||||
|
applicationProperties.getSecurity().getInitialLogin().getPassword();
|
||||||
|
|
||||||
|
if (initialUsername != null
|
||||||
|
&& !initialUsername.isEmpty()
|
||||||
|
&& initialPassword != null
|
||||||
|
&& !initialPassword.isEmpty()
|
||||||
|
&& !userService.findByUsernameIgnoreCase(initialUsername).isPresent()) {
|
||||||
|
try {
|
||||||
|
userService.saveUser(initialUsername, initialPassword, Role.ADMIN.getRoleId());
|
||||||
|
logger.info("Admin user created: " + initialUsername);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
logger.error("Failed to initialize security setup", e);
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
createDefaultAdminUser();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createDefaultAdminUser() {
|
||||||
|
String defaultUsername = "admin";
|
||||||
|
String defaultPassword = "stirling";
|
||||||
|
if (!userService.findByUsernameIgnoreCase(defaultUsername).isPresent()) {
|
||||||
|
userService.saveUser(defaultUsername, defaultPassword, Role.ADMIN.getRoleId(), true);
|
||||||
|
logger.info("Default admin user created: " + defaultUsername);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initializeInternalApiUser() {
|
||||||
|
if (!userService.usernameExistsIgnoreCase(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());
|
||||||
|
logger.info("Internal API user created: " + Role.INTERNAL_API_USER.getRoleId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void saveKeyToConfig(String key) throws IOException {
|
private void saveKeyToConfig(String key) throws IOException {
|
||||||
Path path = Paths.get("configs", "settings.yml"); // Target the configs/settings.yml
|
Path path = Paths.get("configs", "settings.yml"); // Target the configs/settings.yml
|
||||||
List<String> lines = Files.readAllLines(path);
|
List<String> lines = Files.readAllLines(path);
|
||||||
@@ -85,4 +114,16 @@ public class InitialSecuritySetup {
|
|||||||
// Write back to the file
|
// Write back to the file
|
||||||
Files.write(path, lines);
|
Files.write(path, lines);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isValidUUID(String uuid) {
|
||||||
|
if (uuid == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
UUID.fromString(uuid);
|
||||||
|
return true;
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package stirling.software.SPDF.config.security;
|
|||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
@@ -15,44 +17,62 @@ public class LoginAttemptService {
|
|||||||
|
|
||||||
@Autowired ApplicationProperties applicationProperties;
|
@Autowired ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
private int MAX_ATTEMPTS;
|
private static final Logger logger = LoggerFactory.getLogger(LoginAttemptService.class);
|
||||||
|
|
||||||
|
private int MAX_ATTEMPT;
|
||||||
private long ATTEMPT_INCREMENT_TIME;
|
private long ATTEMPT_INCREMENT_TIME;
|
||||||
|
private ConcurrentHashMap<String, AttemptCounter> attemptsCache;
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
public void init() {
|
public void init() {
|
||||||
MAX_ATTEMPTS = applicationProperties.getSecurity().getLoginAttemptCount();
|
MAX_ATTEMPT = applicationProperties.getSecurity().getLoginAttemptCount();
|
||||||
ATTEMPT_INCREMENT_TIME =
|
ATTEMPT_INCREMENT_TIME =
|
||||||
TimeUnit.MINUTES.toMillis(
|
TimeUnit.MINUTES.toMillis(
|
||||||
applicationProperties.getSecurity().getLoginResetTimeMinutes());
|
applicationProperties.getSecurity().getLoginResetTimeMinutes());
|
||||||
|
attemptsCache = new ConcurrentHashMap<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
private final ConcurrentHashMap<String, AttemptCounter> attemptsCache =
|
|
||||||
new ConcurrentHashMap<>();
|
|
||||||
|
|
||||||
public void loginSucceeded(String key) {
|
public void loginSucceeded(String key) {
|
||||||
attemptsCache.remove(key);
|
logger.info(key + " " + attemptsCache.mappingCount());
|
||||||
|
if (key == null || key.trim().isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
attemptsCache.remove(key.toLowerCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean loginAttemptCheck(String key) {
|
public void loginFailed(String key) {
|
||||||
attemptsCache.compute(
|
if (key == null || key.trim().isEmpty()) return;
|
||||||
key,
|
|
||||||
(k, attemptCounter) -> {
|
AttemptCounter attemptCounter = attemptsCache.get(key.toLowerCase());
|
||||||
if (attemptCounter == null
|
if (attemptCounter == null) {
|
||||||
|| attemptCounter.shouldReset(ATTEMPT_INCREMENT_TIME)) {
|
attemptCounter = new AttemptCounter();
|
||||||
return new AttemptCounter();
|
attemptsCache.put(key.toLowerCase(), attemptCounter);
|
||||||
} else {
|
} else {
|
||||||
attemptCounter.increment();
|
if (attemptCounter.shouldReset(ATTEMPT_INCREMENT_TIME)) {
|
||||||
return attemptCounter;
|
attemptCounter.reset();
|
||||||
}
|
}
|
||||||
});
|
attemptCounter.increment();
|
||||||
return attemptsCache.get(key).getAttemptCount() >= MAX_ATTEMPTS;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isBlocked(String key) {
|
public boolean isBlocked(String key) {
|
||||||
AttemptCounter attemptCounter = attemptsCache.get(key);
|
if (key == null || key.trim().isEmpty()) return false;
|
||||||
if (attemptCounter != null) {
|
AttemptCounter attemptCounter = attemptsCache.get(key.toLowerCase());
|
||||||
return attemptCounter.getAttemptCount() >= MAX_ATTEMPTS;
|
if (attemptCounter == null) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
|
return attemptCounter.getAttemptCount() >= MAX_ATTEMPT;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getRemainingAttempts(String key) {
|
||||||
|
if (key == null || key.trim().isEmpty()) return MAX_ATTEMPT;
|
||||||
|
|
||||||
|
AttemptCounter attemptCounter = attemptsCache.get(key.toLowerCase());
|
||||||
|
if (attemptCounter == null) {
|
||||||
|
return MAX_ATTEMPT;
|
||||||
|
}
|
||||||
|
|
||||||
|
return MAX_ATTEMPT - attemptCounter.getAttemptCount();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
package stirling.software.SPDF.config.security;
|
package stirling.software.SPDF.config.security;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.context.annotation.Lazy;
|
import org.springframework.context.annotation.Lazy;
|
||||||
@@ -9,15 +12,32 @@ import org.springframework.security.authentication.dao.DaoAuthenticationProvider
|
|||||||
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
||||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||||
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
|
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
|
||||||
|
import org.springframework.security.core.session.SessionRegistry;
|
||||||
|
import org.springframework.security.core.session.SessionRegistryImpl;
|
||||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||||
|
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
||||||
|
import org.springframework.security.oauth2.client.registration.ClientRegistrations;
|
||||||
|
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
|
||||||
|
import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
|
||||||
import org.springframework.security.web.SecurityFilterChain;
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||||
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
|
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
|
||||||
import org.springframework.security.web.savedrequest.NullRequestCache;
|
import org.springframework.security.web.savedrequest.NullRequestCache;
|
||||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationFailureHandler;
|
||||||
|
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationSuccessHandler;
|
||||||
|
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2LogoutSuccessHandler;
|
||||||
|
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2UserService;
|
||||||
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
|
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
|
||||||
|
import stirling.software.SPDF.model.User;
|
||||||
import stirling.software.SPDF.repository.JPATokenRepositoryImpl;
|
import stirling.software.SPDF.repository.JPATokenRepositoryImpl;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@@ -25,7 +45,7 @@ import stirling.software.SPDF.repository.JPATokenRepositoryImpl;
|
|||||||
@EnableMethodSecurity
|
@EnableMethodSecurity
|
||||||
public class SecurityConfiguration {
|
public class SecurityConfiguration {
|
||||||
|
|
||||||
@Autowired private UserDetailsService userDetailsService;
|
@Autowired private CustomUserDetailsService userDetailsService;
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public PasswordEncoder passwordEncoder() {
|
public PasswordEncoder passwordEncoder() {
|
||||||
@@ -38,12 +58,19 @@ public class SecurityConfiguration {
|
|||||||
@Qualifier("loginEnabled")
|
@Qualifier("loginEnabled")
|
||||||
public boolean loginEnabledValue;
|
public boolean loginEnabledValue;
|
||||||
|
|
||||||
|
@Autowired ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
@Autowired private UserAuthenticationFilter userAuthenticationFilter;
|
@Autowired private UserAuthenticationFilter userAuthenticationFilter;
|
||||||
|
|
||||||
@Autowired private LoginAttemptService loginAttemptService;
|
@Autowired private LoginAttemptService loginAttemptService;
|
||||||
|
|
||||||
@Autowired private FirstLoginFilter firstLoginFilter;
|
@Autowired private FirstLoginFilter firstLoginFilter;
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public SessionRegistry sessionRegistry() {
|
||||||
|
return new SessionRegistryImpl();
|
||||||
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||||
http.addFilterBefore(userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
http.addFilterBefore(userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
||||||
@@ -53,23 +80,33 @@ public class SecurityConfiguration {
|
|||||||
http.csrf(csrf -> csrf.disable());
|
http.csrf(csrf -> csrf.disable());
|
||||||
http.addFilterBefore(rateLimitingFilter(), UsernamePasswordAuthenticationFilter.class);
|
http.addFilterBefore(rateLimitingFilter(), UsernamePasswordAuthenticationFilter.class);
|
||||||
http.addFilterAfter(firstLoginFilter, UsernamePasswordAuthenticationFilter.class);
|
http.addFilterAfter(firstLoginFilter, UsernamePasswordAuthenticationFilter.class);
|
||||||
|
http.sessionManagement(
|
||||||
|
sessionManagement ->
|
||||||
|
sessionManagement
|
||||||
|
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
|
||||||
|
.maximumSessions(10)
|
||||||
|
.maxSessionsPreventsLogin(false)
|
||||||
|
.sessionRegistry(sessionRegistry())
|
||||||
|
.expiredUrl("/login?logout=true"));
|
||||||
|
|
||||||
http.formLogin(
|
http.formLogin(
|
||||||
formLogin ->
|
formLogin ->
|
||||||
formLogin
|
formLogin
|
||||||
.loginPage("/login")
|
.loginPage("/login")
|
||||||
.successHandler(
|
.successHandler(
|
||||||
new CustomAuthenticationSuccessHandler())
|
new CustomAuthenticationSuccessHandler(
|
||||||
|
loginAttemptService))
|
||||||
.defaultSuccessUrl("/")
|
.defaultSuccessUrl("/")
|
||||||
.failureHandler(
|
.failureHandler(
|
||||||
new CustomAuthenticationFailureHandler(
|
new CustomAuthenticationFailureHandler(
|
||||||
loginAttemptService))
|
loginAttemptService, userService))
|
||||||
.permitAll())
|
.permitAll())
|
||||||
.requestCache(requestCache -> requestCache.requestCache(new NullRequestCache()))
|
.requestCache(requestCache -> requestCache.requestCache(new NullRequestCache()))
|
||||||
.logout(
|
.logout(
|
||||||
logout ->
|
logout ->
|
||||||
logout.logoutRequestMatcher(
|
logout.logoutRequestMatcher(
|
||||||
new AntPathRequestMatcher("/logout"))
|
new AntPathRequestMatcher("/logout"))
|
||||||
.logoutSuccessUrl("/login?logout=true")
|
.logoutSuccessHandler(new CustomLogoutSuccessHandler())
|
||||||
.invalidateHttpSession(true) // Invalidate session
|
.invalidateHttpSession(true) // Invalidate session
|
||||||
.deleteCookies("JSESSIONID", "remember-me"))
|
.deleteCookies("JSESSIONID", "remember-me"))
|
||||||
.rememberMe(
|
.rememberMe(
|
||||||
@@ -95,6 +132,7 @@ public class SecurityConfiguration {
|
|||||||
: uri;
|
: uri;
|
||||||
|
|
||||||
return trimmedUri.startsWith("/login")
|
return trimmedUri.startsWith("/login")
|
||||||
|
|| trimmedUri.startsWith("/oauth")
|
||||||
|| trimmedUri.endsWith(".svg")
|
|| trimmedUri.endsWith(".svg")
|
||||||
|| trimmedUri.startsWith(
|
|| trimmedUri.startsWith(
|
||||||
"/register")
|
"/register")
|
||||||
@@ -109,8 +147,44 @@ public class SecurityConfiguration {
|
|||||||
.permitAll()
|
.permitAll()
|
||||||
.anyRequest()
|
.anyRequest()
|
||||||
.authenticated())
|
.authenticated())
|
||||||
.userDetailsService(userDetailsService)
|
|
||||||
.authenticationProvider(authenticationProvider());
|
.authenticationProvider(authenticationProvider());
|
||||||
|
|
||||||
|
// Handle OAUTH2 Logins
|
||||||
|
if (applicationProperties.getSecurity().getOAUTH2().getEnabled()) {
|
||||||
|
|
||||||
|
http.oauth2Login(
|
||||||
|
oauth2 ->
|
||||||
|
oauth2.loginPage("/oauth2")
|
||||||
|
/*
|
||||||
|
This Custom handler is used to check if the OAUTH2 user trying to log in, already exists in the database.
|
||||||
|
If user exists, login proceeds as usual. If user does not exist, then it is autocreated but only if 'OAUTH2AutoCreateUser'
|
||||||
|
is set as true, else login fails with an error message advising the same.
|
||||||
|
*/
|
||||||
|
.successHandler(
|
||||||
|
new CustomOAuth2AuthenticationSuccessHandler(
|
||||||
|
loginAttemptService,
|
||||||
|
applicationProperties,
|
||||||
|
userService))
|
||||||
|
.failureHandler(
|
||||||
|
new CustomOAuth2AuthenticationFailureHandler())
|
||||||
|
// Add existing Authorities from the database
|
||||||
|
.userInfoEndpoint(
|
||||||
|
userInfoEndpoint ->
|
||||||
|
userInfoEndpoint
|
||||||
|
.oidcUserService(
|
||||||
|
new CustomOAuth2UserService(
|
||||||
|
applicationProperties,
|
||||||
|
userService,
|
||||||
|
loginAttemptService))
|
||||||
|
.userAuthoritiesMapper(
|
||||||
|
userAuthoritiesMapper())))
|
||||||
|
.logout(
|
||||||
|
logout ->
|
||||||
|
logout.logoutSuccessHandler(
|
||||||
|
new CustomOAuth2LogoutSuccessHandler(
|
||||||
|
this.applicationProperties,
|
||||||
|
sessionRegistry())));
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
http.csrf(csrf -> csrf.disable())
|
http.csrf(csrf -> csrf.disable())
|
||||||
.authorizeHttpRequests(authz -> authz.anyRequest().permitAll());
|
.authorizeHttpRequests(authz -> authz.anyRequest().permitAll());
|
||||||
@@ -119,6 +193,70 @@ public class SecurityConfiguration {
|
|||||||
return http.build();
|
return http.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Client Registration Repository for OAUTH2 OIDC Login
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnProperty(
|
||||||
|
value = "security.oauth2.enabled",
|
||||||
|
havingValue = "true",
|
||||||
|
matchIfMissing = false)
|
||||||
|
public ClientRegistrationRepository clientRegistrationRepository() {
|
||||||
|
return new InMemoryClientRegistrationRepository(this.oidcClientRegistration());
|
||||||
|
}
|
||||||
|
|
||||||
|
private ClientRegistration oidcClientRegistration() {
|
||||||
|
OAUTH2 oauth = applicationProperties.getSecurity().getOAUTH2();
|
||||||
|
return ClientRegistrations.fromIssuerLocation(oauth.getIssuer())
|
||||||
|
.registrationId("oidc")
|
||||||
|
.clientId(oauth.getClientId())
|
||||||
|
.clientSecret(oauth.getClientSecret())
|
||||||
|
.scope(oauth.getScopes())
|
||||||
|
.userNameAttributeName(oauth.getUseAsUsername())
|
||||||
|
.clientName("OIDC")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
This following function is to grant Authorities to the OAUTH2 user from the values stored in the database.
|
||||||
|
This is required for the internal; 'hasRole()' function to give out the correct role.
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnProperty(
|
||||||
|
value = "security.oauth2.enabled",
|
||||||
|
havingValue = "true",
|
||||||
|
matchIfMissing = false)
|
||||||
|
GrantedAuthoritiesMapper userAuthoritiesMapper() {
|
||||||
|
return (authorities) -> {
|
||||||
|
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
|
||||||
|
|
||||||
|
authorities.forEach(
|
||||||
|
authority -> {
|
||||||
|
// Add existing OAUTH2 Authorities
|
||||||
|
mappedAuthorities.add(new SimpleGrantedAuthority(authority.getAuthority()));
|
||||||
|
|
||||||
|
// Add Authorities from database for existing user, if user is present.
|
||||||
|
if (authority instanceof OAuth2UserAuthority oauth2Auth) {
|
||||||
|
String useAsUsername =
|
||||||
|
applicationProperties
|
||||||
|
.getSecurity()
|
||||||
|
.getOAUTH2()
|
||||||
|
.getUseAsUsername();
|
||||||
|
Optional<User> userOpt =
|
||||||
|
userService.findByUsernameIgnoreCase(
|
||||||
|
(String) oauth2Auth.getAttributes().get(useAsUsername));
|
||||||
|
if (userOpt.isPresent()) {
|
||||||
|
User user = userOpt.get();
|
||||||
|
if (user != null) {
|
||||||
|
mappedAuthorities.add(
|
||||||
|
new SimpleGrantedAuthority(
|
||||||
|
userService.findRole(user).getAuthority()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return mappedAuthorities;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public IPRateLimitingFilter rateLimitingFilter() {
|
public IPRateLimitingFilter rateLimitingFilter() {
|
||||||
int maxRequestsPerIp = 1000000; // Example limit TODO add config level
|
int maxRequestsPerIp = 1000000; // Example limit TODO add config level
|
||||||
@@ -137,4 +275,9 @@ public class SecurityConfiguration {
|
|||||||
public PersistentTokenRepository persistentTokenRepository() {
|
public PersistentTokenRepository persistentTokenRepository() {
|
||||||
return new JPATokenRepositoryImpl();
|
return new JPATokenRepositoryImpl();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public boolean activSecurity() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
|
|||||||
response.setStatus(HttpStatus.UNAUTHORIZED.value());
|
response.setStatus(HttpStatus.UNAUTHORIZED.value());
|
||||||
response.getWriter()
|
response.getWriter()
|
||||||
.write(
|
.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");
|
"Authentication required. Please provide a X-API-KEY in request header.\nThis is found in Settings -> Account Settings -> API Key\nAlternatively you can disable authentication if this is unexpected");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -115,4 +115,4 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
|
|||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import io.github.bucket4j.Bandwidth;
|
|||||||
import io.github.bucket4j.Bucket;
|
import io.github.bucket4j.Bucket;
|
||||||
import io.github.bucket4j.ConsumptionProbe;
|
import io.github.bucket4j.ConsumptionProbe;
|
||||||
import io.github.bucket4j.Refill;
|
import io.github.bucket4j.Refill;
|
||||||
|
import io.github.pixee.security.Newlines;
|
||||||
|
|
||||||
import jakarta.servlet.FilterChain;
|
import jakarta.servlet.FilterChain;
|
||||||
import jakarta.servlet.ServletException;
|
import jakarta.servlet.ServletException;
|
||||||
@@ -125,12 +126,16 @@ public class UserBasedRateLimitingFilter extends OncePerRequestFilter {
|
|||||||
ConsumptionProbe probe = userBucket.tryConsumeAndReturnRemaining(1);
|
ConsumptionProbe probe = userBucket.tryConsumeAndReturnRemaining(1);
|
||||||
|
|
||||||
if (probe.isConsumed()) {
|
if (probe.isConsumed()) {
|
||||||
response.setHeader("X-Rate-Limit-Remaining", Long.toString(probe.getRemainingTokens()));
|
response.setHeader(
|
||||||
|
"X-Rate-Limit-Remaining",
|
||||||
|
Newlines.stripAll(Long.toString(probe.getRemainingTokens())));
|
||||||
filterChain.doFilter(request, response);
|
filterChain.doFilter(request, response);
|
||||||
} else {
|
} else {
|
||||||
long waitForRefill = probe.getNanosToWaitForRefill() / 1_000_000_000;
|
long waitForRefill = probe.getNanosToWaitForRefill() / 1_000_000_000;
|
||||||
response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
|
response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
|
||||||
response.setHeader("X-Rate-Limit-Retry-After-Seconds", String.valueOf(waitForRefill));
|
response.setHeader(
|
||||||
|
"X-Rate-Limit-Retry-After-Seconds",
|
||||||
|
Newlines.stripAll(String.valueOf(waitForRefill)));
|
||||||
response.getWriter().write("Rate limit exceeded for POST requests.");
|
response.getWriter().write("Rate limit exceeded for POST requests.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import java.util.UUID;
|
|||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.MessageSource;
|
||||||
|
import org.springframework.context.i18n.LocaleContextHolder;
|
||||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.core.GrantedAuthority;
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
@@ -18,9 +20,11 @@ import org.springframework.security.crypto.password.PasswordEncoder;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import stirling.software.SPDF.controller.api.pipeline.UserServiceInterface;
|
import stirling.software.SPDF.controller.api.pipeline.UserServiceInterface;
|
||||||
|
import stirling.software.SPDF.model.AuthenticationType;
|
||||||
import stirling.software.SPDF.model.Authority;
|
import stirling.software.SPDF.model.Authority;
|
||||||
import stirling.software.SPDF.model.Role;
|
import stirling.software.SPDF.model.Role;
|
||||||
import stirling.software.SPDF.model.User;
|
import stirling.software.SPDF.model.User;
|
||||||
|
import stirling.software.SPDF.repository.AuthorityRepository;
|
||||||
import stirling.software.SPDF.repository.UserRepository;
|
import stirling.software.SPDF.repository.UserRepository;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@@ -28,8 +32,28 @@ public class UserService implements UserServiceInterface {
|
|||||||
|
|
||||||
@Autowired private UserRepository userRepository;
|
@Autowired private UserRepository userRepository;
|
||||||
|
|
||||||
|
@Autowired private AuthorityRepository authorityRepository;
|
||||||
|
|
||||||
@Autowired private PasswordEncoder passwordEncoder;
|
@Autowired private PasswordEncoder passwordEncoder;
|
||||||
|
|
||||||
|
@Autowired private MessageSource messageSource;
|
||||||
|
|
||||||
|
// Handle OAUTH2 login and user auto creation.
|
||||||
|
public boolean processOAuth2PostLogin(String username, boolean autoCreateUser) {
|
||||||
|
if (!isUsernameValid(username)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Optional<User> existingUser = userRepository.findByUsernameIgnoreCase(username);
|
||||||
|
if (existingUser.isPresent()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (autoCreateUser) {
|
||||||
|
saveUser(username, AuthenticationType.OAUTH2);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
public Authentication getAuthentication(String apiKey) {
|
public Authentication getAuthentication(String apiKey) {
|
||||||
User user = getUserByApiKey(apiKey);
|
User user = getUserByApiKey(apiKey);
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
@@ -62,7 +86,7 @@ public class UserService implements UserServiceInterface {
|
|||||||
public User addApiKeyToUser(String username) {
|
public User addApiKeyToUser(String username) {
|
||||||
User user =
|
User user =
|
||||||
userRepository
|
userRepository
|
||||||
.findByUsername(username)
|
.findByUsernameIgnoreCase(username)
|
||||||
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
|
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
|
||||||
|
|
||||||
user.setApiKey(generateApiKey());
|
user.setApiKey(generateApiKey());
|
||||||
@@ -76,7 +100,7 @@ public class UserService implements UserServiceInterface {
|
|||||||
public String getApiKeyForUser(String username) {
|
public String getApiKeyForUser(String username) {
|
||||||
User user =
|
User user =
|
||||||
userRepository
|
userRepository
|
||||||
.findByUsername(username)
|
.findByUsernameIgnoreCase(username)
|
||||||
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
|
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
|
||||||
return user.getApiKey();
|
return user.getApiKey();
|
||||||
}
|
}
|
||||||
@@ -90,9 +114,8 @@ public class UserService implements UserServiceInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public UserDetails loadUserByApiKey(String apiKey) {
|
public UserDetails loadUserByApiKey(String apiKey) {
|
||||||
User userOptional = userRepository.findByApiKey(apiKey);
|
User user = userRepository.findByApiKey(apiKey);
|
||||||
if (userOptional != null) {
|
if (user != null) {
|
||||||
User user = userOptional;
|
|
||||||
// Convert your User entity to a UserDetails object with authorities
|
// Convert your User entity to a UserDetails object with authorities
|
||||||
return new org.springframework.security.core.userdetails.User(
|
return new org.springframework.security.core.userdetails.User(
|
||||||
user.getUsername(),
|
user.getUsername(),
|
||||||
@@ -103,40 +126,58 @@ public class UserService implements UserServiceInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean validateApiKeyForUser(String username, String apiKey) {
|
public boolean validateApiKeyForUser(String username, String apiKey) {
|
||||||
Optional<User> userOpt = userRepository.findByUsername(username);
|
Optional<User> userOpt = userRepository.findByUsernameIgnoreCase(username);
|
||||||
return userOpt.isPresent() && userOpt.get().getApiKey().equals(apiKey);
|
return userOpt.isPresent() && apiKey.equals(userOpt.get().getApiKey());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void saveUser(String username, String password) {
|
public void saveUser(String username, AuthenticationType authenticationType)
|
||||||
|
throws IllegalArgumentException {
|
||||||
|
if (!isUsernameValid(username)) {
|
||||||
|
throw new IllegalArgumentException(getInvalidUsernameMessage());
|
||||||
|
}
|
||||||
|
User user = new User();
|
||||||
|
user.setUsername(username);
|
||||||
|
user.setEnabled(true);
|
||||||
|
user.setFirstLogin(false);
|
||||||
|
user.addAuthority(new Authority(Role.USER.getRoleId(), user));
|
||||||
|
user.setAuthenticationType(authenticationType);
|
||||||
|
userRepository.save(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void saveUser(String username, String password) throws IllegalArgumentException {
|
||||||
|
if (!isUsernameValid(username)) {
|
||||||
|
throw new IllegalArgumentException(getInvalidUsernameMessage());
|
||||||
|
}
|
||||||
User user = new User();
|
User user = new User();
|
||||||
user.setUsername(username);
|
user.setUsername(username);
|
||||||
user.setPassword(passwordEncoder.encode(password));
|
user.setPassword(passwordEncoder.encode(password));
|
||||||
user.setEnabled(true);
|
user.setEnabled(true);
|
||||||
|
user.setAuthenticationType(AuthenticationType.WEB);
|
||||||
userRepository.save(user);
|
userRepository.save(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void saveUser(String username, String password, String role, boolean firstLogin) {
|
public void saveUser(String username, String password, String role, boolean firstLogin)
|
||||||
|
throws IllegalArgumentException {
|
||||||
|
if (!isUsernameValid(username)) {
|
||||||
|
throw new IllegalArgumentException(getInvalidUsernameMessage());
|
||||||
|
}
|
||||||
User user = new User();
|
User user = new User();
|
||||||
user.setUsername(username);
|
user.setUsername(username);
|
||||||
user.setPassword(passwordEncoder.encode(password));
|
user.setPassword(passwordEncoder.encode(password));
|
||||||
user.addAuthority(new Authority(role, user));
|
user.addAuthority(new Authority(role, user));
|
||||||
user.setEnabled(true);
|
user.setEnabled(true);
|
||||||
|
user.setAuthenticationType(AuthenticationType.WEB);
|
||||||
user.setFirstLogin(firstLogin);
|
user.setFirstLogin(firstLogin);
|
||||||
userRepository.save(user);
|
userRepository.save(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void saveUser(String username, String password, String role) {
|
public void saveUser(String username, String password, String role)
|
||||||
User user = new User();
|
throws IllegalArgumentException {
|
||||||
user.setUsername(username);
|
saveUser(username, password, role, false);
|
||||||
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) {
|
public void deleteUser(String username) {
|
||||||
Optional<User> userOpt = userRepository.findByUsername(username);
|
Optional<User> userOpt = userRepository.findByUsernameIgnoreCase(username);
|
||||||
if (userOpt.isPresent()) {
|
if (userOpt.isPresent()) {
|
||||||
for (Authority authority : userOpt.get().getAuthorities()) {
|
for (Authority authority : userOpt.get().getAuthorities()) {
|
||||||
if (authority.getAuthority().equals(Role.INTERNAL_API_USER.getRoleId())) {
|
if (authority.getAuthority().equals(Role.INTERNAL_API_USER.getRoleId())) {
|
||||||
@@ -151,18 +192,28 @@ public class UserService implements UserServiceInterface {
|
|||||||
return userRepository.findByUsername(username).isPresent();
|
return userRepository.findByUsername(username).isPresent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean usernameExistsIgnoreCase(String username) {
|
||||||
|
return userRepository.findByUsernameIgnoreCase(username).isPresent();
|
||||||
|
}
|
||||||
|
|
||||||
public boolean hasUsers() {
|
public boolean hasUsers() {
|
||||||
return userRepository.count() > 0;
|
long userCount = userRepository.count();
|
||||||
|
if (userRepository
|
||||||
|
.findByUsernameIgnoreCase(Role.INTERNAL_API_USER.getRoleId())
|
||||||
|
.isPresent()) {
|
||||||
|
userCount -= 1;
|
||||||
|
}
|
||||||
|
return userCount > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void updateUserSettings(String username, Map<String, String> updates) {
|
public void updateUserSettings(String username, Map<String, String> updates) {
|
||||||
Optional<User> userOpt = userRepository.findByUsername(username);
|
Optional<User> userOpt = userRepository.findByUsernameIgnoreCase(username);
|
||||||
if (userOpt.isPresent()) {
|
if (userOpt.isPresent()) {
|
||||||
User user = userOpt.get();
|
User user = userOpt.get();
|
||||||
Map<String, String> settingsMap = user.getSettings();
|
Map<String, String> settingsMap = user.getSettings();
|
||||||
|
|
||||||
if (settingsMap == null) {
|
if (settingsMap == null) {
|
||||||
settingsMap = new HashMap<String, String>();
|
settingsMap = new HashMap<>();
|
||||||
}
|
}
|
||||||
settingsMap.clear();
|
settingsMap.clear();
|
||||||
settingsMap.putAll(updates);
|
settingsMap.putAll(updates);
|
||||||
@@ -176,7 +227,18 @@ public class UserService implements UserServiceInterface {
|
|||||||
return userRepository.findByUsername(username);
|
return userRepository.findByUsername(username);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void changeUsername(User user, String newUsername) {
|
public Optional<User> findByUsernameIgnoreCase(String username) {
|
||||||
|
return userRepository.findByUsernameIgnoreCase(username);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Authority findRole(User user) {
|
||||||
|
return authorityRepository.findByUserId(user.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void changeUsername(User user, String newUsername) throws IllegalArgumentException {
|
||||||
|
if (!isUsernameValid(newUsername)) {
|
||||||
|
throw new IllegalArgumentException(getInvalidUsernameMessage());
|
||||||
|
}
|
||||||
user.setUsername(newUsername);
|
user.setUsername(newUsername);
|
||||||
userRepository.save(user);
|
userRepository.save(user);
|
||||||
}
|
}
|
||||||
@@ -191,7 +253,41 @@ public class UserService implements UserServiceInterface {
|
|||||||
userRepository.save(user);
|
userRepository.save(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void changeRole(User user, String newRole) {
|
||||||
|
Authority userAuthority = this.findRole(user);
|
||||||
|
userAuthority.setAuthority(newRole);
|
||||||
|
authorityRepository.save(userAuthority);
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isPasswordCorrect(User user, String currentPassword) {
|
public boolean isPasswordCorrect(User user, String currentPassword) {
|
||||||
return passwordEncoder.matches(currentPassword, user.getPassword());
|
return passwordEncoder.matches(currentPassword, user.getPassword());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isUsernameValid(String username) {
|
||||||
|
// Checks whether the simple username is formatted correctly
|
||||||
|
boolean isValidSimpleUsername =
|
||||||
|
username.matches("^[a-zA-Z0-9][a-zA-Z0-9@._+-]*[a-zA-Z0-9]$");
|
||||||
|
// Checks whether the email address is formatted correctly
|
||||||
|
boolean isValidEmail =
|
||||||
|
username.matches(
|
||||||
|
"^(?=.{1,64}@)[A-Za-z0-9]+(\\.[A-Za-z0-9_+.-]+)*@[^-][A-Za-z0-9-]+(\\.[A-Za-z0-9-]+)*(\\.[A-Za-z]{2,})$");
|
||||||
|
return isValidSimpleUsername || isValidEmail;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getInvalidUsernameMessage() {
|
||||||
|
return messageSource.getMessage(
|
||||||
|
"invalidUsernameMessage", null, LocaleContextHolder.getLocale());
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasPassword(String username) {
|
||||||
|
Optional<User> user = userRepository.findByUsernameIgnoreCase(username);
|
||||||
|
return user.isPresent() && user.get().hasPassword();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isAuthenticationTypeByUsername(
|
||||||
|
String username, AuthenticationType authenticationType) {
|
||||||
|
Optional<User> user = userRepository.findByUsernameIgnoreCase(username);
|
||||||
|
return user.isPresent()
|
||||||
|
&& authenticationType.name().equalsIgnoreCase(user.get().getAuthenticationType());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package stirling.software.SPDF.config.security.oauth2;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.security.authentication.LockedException;
|
||||||
|
import org.springframework.security.core.AuthenticationException;
|
||||||
|
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||||
|
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||||
|
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
|
||||||
|
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
public class CustomOAuth2AuthenticationFailureHandler
|
||||||
|
extends SimpleUrlAuthenticationFailureHandler {
|
||||||
|
|
||||||
|
private static final Logger logger =
|
||||||
|
LoggerFactory.getLogger(CustomOAuth2AuthenticationFailureHandler.class);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAuthenticationFailure(
|
||||||
|
HttpServletRequest request,
|
||||||
|
HttpServletResponse response,
|
||||||
|
AuthenticationException exception)
|
||||||
|
throws IOException, ServletException {
|
||||||
|
if (exception instanceof OAuth2AuthenticationException) {
|
||||||
|
OAuth2Error error = ((OAuth2AuthenticationException) exception).getError();
|
||||||
|
|
||||||
|
String errorCode = error.getErrorCode();
|
||||||
|
|
||||||
|
if (error.getErrorCode().equals("Password must not be null")) {
|
||||||
|
errorCode = "userAlreadyExistsWeb";
|
||||||
|
}
|
||||||
|
logger.error("OAuth2 Authentication error: " + errorCode);
|
||||||
|
getRedirectStrategy()
|
||||||
|
.sendRedirect(request, response, "/logout?erroroauth=" + errorCode);
|
||||||
|
return;
|
||||||
|
} else if (exception instanceof LockedException) {
|
||||||
|
logger.error("Account locked: ", exception);
|
||||||
|
getRedirectStrategy().sendRedirect(request, response, "/logout?error=locked");
|
||||||
|
} else {
|
||||||
|
logger.error("Unhandled authentication exception", exception);
|
||||||
|
super.onAuthenticationFailure(request, response, exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
package stirling.software.SPDF.config.security.oauth2;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.security.authentication.LockedException;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||||
|
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
|
||||||
|
import org.springframework.security.web.savedrequest.SavedRequest;
|
||||||
|
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import jakarta.servlet.http.HttpSession;
|
||||||
|
import stirling.software.SPDF.config.security.LoginAttemptService;
|
||||||
|
import stirling.software.SPDF.config.security.UserService;
|
||||||
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
|
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
|
||||||
|
import stirling.software.SPDF.model.AuthenticationType;
|
||||||
|
import stirling.software.SPDF.utils.RequestUriUtils;
|
||||||
|
|
||||||
|
public class CustomOAuth2AuthenticationSuccessHandler
|
||||||
|
extends SavedRequestAwareAuthenticationSuccessHandler {
|
||||||
|
|
||||||
|
private LoginAttemptService loginAttemptService;
|
||||||
|
|
||||||
|
private static final Logger logger =
|
||||||
|
LoggerFactory.getLogger(CustomOAuth2AuthenticationSuccessHandler.class);
|
||||||
|
|
||||||
|
private ApplicationProperties applicationProperties;
|
||||||
|
private UserService userService;
|
||||||
|
|
||||||
|
public CustomOAuth2AuthenticationSuccessHandler(
|
||||||
|
final LoginAttemptService loginAttemptService,
|
||||||
|
ApplicationProperties applicationProperties,
|
||||||
|
UserService userService) {
|
||||||
|
this.applicationProperties = applicationProperties;
|
||||||
|
this.userService = userService;
|
||||||
|
this.loginAttemptService = loginAttemptService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAuthenticationSuccess(
|
||||||
|
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
|
||||||
|
throws ServletException, IOException {
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
OAuth2User oauthUser = (OAuth2User) authentication.getPrincipal();
|
||||||
|
OAUTH2 oAuth = applicationProperties.getSecurity().getOAUTH2();
|
||||||
|
|
||||||
|
String username = oauthUser.getName();
|
||||||
|
|
||||||
|
if (loginAttemptService.isBlocked(username)) {
|
||||||
|
if (session != null) {
|
||||||
|
session.removeAttribute("SPRING_SECURITY_SAVED_REQUEST");
|
||||||
|
}
|
||||||
|
throw new LockedException(
|
||||||
|
"Your account has been locked due to too many failed login attempts.");
|
||||||
|
}
|
||||||
|
if (userService.usernameExistsIgnoreCase(username)
|
||||||
|
&& userService.hasPassword(username)
|
||||||
|
&& !userService.isAuthenticationTypeByUsername(
|
||||||
|
username, AuthenticationType.OAUTH2)
|
||||||
|
&& oAuth.getAutoCreateUser()) {
|
||||||
|
response.sendRedirect(
|
||||||
|
request.getContextPath() + "/logout?oauth2AuthenticationErrorWeb=true");
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
userService.processOAuth2PostLogin(username, oAuth.getAutoCreateUser());
|
||||||
|
response.sendRedirect("/");
|
||||||
|
return;
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
response.sendRedirect("/logout?invalidUsername=true");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package stirling.software.SPDF.config.security.oauth2;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.session.SessionRegistry;
|
||||||
|
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
|
||||||
|
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import jakarta.servlet.http.HttpSession;
|
||||||
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
|
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
|
||||||
|
|
||||||
|
public class CustomOAuth2LogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
|
||||||
|
|
||||||
|
private static final Logger logger =
|
||||||
|
LoggerFactory.getLogger(CustomOAuth2LogoutSuccessHandler.class);
|
||||||
|
|
||||||
|
private final SessionRegistry sessionRegistry;
|
||||||
|
private final ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
|
public CustomOAuth2LogoutSuccessHandler(
|
||||||
|
ApplicationProperties applicationProperties, SessionRegistry sessionRegistry) {
|
||||||
|
this.sessionRegistry = sessionRegistry;
|
||||||
|
this.applicationProperties = applicationProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onLogoutSuccess(
|
||||||
|
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
|
||||||
|
throws IOException, ServletException {
|
||||||
|
|
||||||
|
String param = "logout=true";
|
||||||
|
|
||||||
|
OAUTH2 oauth = applicationProperties.getSecurity().getOAUTH2();
|
||||||
|
String provider = oauth.getProvider() != null ? oauth.getProvider() : "";
|
||||||
|
|
||||||
|
if (request.getParameter("oauth2AuthenticationErrorWeb") != null) {
|
||||||
|
param = "erroroauth=oauth2AuthenticationErrorWeb";
|
||||||
|
} else if (request.getParameter("error") != null) {
|
||||||
|
param = "error=" + request.getParameter("error");
|
||||||
|
} else if (request.getParameter("erroroauth") != null) {
|
||||||
|
param = "erroroauth=" + request.getParameter("erroroauth");
|
||||||
|
} else if (request.getParameter("oauth2AutoCreateDisabled") != null) {
|
||||||
|
param = "error=oauth2AutoCreateDisabled";
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpSession session = request.getSession(false);
|
||||||
|
if (session != null) {
|
||||||
|
String sessionId = session.getId();
|
||||||
|
sessionRegistry.removeSessionInformation(sessionId);
|
||||||
|
session.invalidate();
|
||||||
|
logger.debug("Session invalidated: " + sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (provider) {
|
||||||
|
case "keycloak":
|
||||||
|
String logoutUrl =
|
||||||
|
oauth.getIssuer()
|
||||||
|
+ "/protocol/openid-connect/logout"
|
||||||
|
+ "?client_id="
|
||||||
|
+ oauth.getClientId()
|
||||||
|
+ "&post_logout_redirect_uri="
|
||||||
|
+ response.encodeRedirectURL(
|
||||||
|
request.getScheme()
|
||||||
|
+ "://"
|
||||||
|
+ request.getHeader("host")
|
||||||
|
+ "/login?"
|
||||||
|
+ param);
|
||||||
|
logger.debug("Redirecting to Keycloak logout URL: " + logoutUrl);
|
||||||
|
response.sendRedirect(logoutUrl);
|
||||||
|
break;
|
||||||
|
case "google":
|
||||||
|
// Add Google specific logout URL if needed
|
||||||
|
default:
|
||||||
|
String redirectUrl = request.getContextPath() + "/login?" + param;
|
||||||
|
logger.debug("Redirecting to default logout URL: " + redirectUrl);
|
||||||
|
response.sendRedirect(redirectUrl);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package stirling.software.SPDF.config.security.oauth2;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.security.authentication.LockedException;
|
||||||
|
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
|
||||||
|
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
|
||||||
|
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
|
||||||
|
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||||
|
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||||
|
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
|
||||||
|
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.config.security.LoginAttemptService;
|
||||||
|
import stirling.software.SPDF.config.security.UserService;
|
||||||
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
|
import stirling.software.SPDF.model.User;
|
||||||
|
|
||||||
|
public class CustomOAuth2UserService implements OAuth2UserService<OidcUserRequest, OidcUser> {
|
||||||
|
|
||||||
|
private final OidcUserService delegate = new OidcUserService();
|
||||||
|
|
||||||
|
private UserService userService;
|
||||||
|
|
||||||
|
private LoginAttemptService loginAttemptService;
|
||||||
|
|
||||||
|
private ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(CustomOAuth2UserService.class);
|
||||||
|
|
||||||
|
public CustomOAuth2UserService(
|
||||||
|
ApplicationProperties applicationProperties,
|
||||||
|
UserService userService,
|
||||||
|
LoginAttemptService loginAttemptService) {
|
||||||
|
this.applicationProperties = applicationProperties;
|
||||||
|
this.userService = userService;
|
||||||
|
this.loginAttemptService = loginAttemptService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException {
|
||||||
|
String usernameAttribute =
|
||||||
|
applicationProperties.getSecurity().getOAUTH2().getUseAsUsername();
|
||||||
|
try {
|
||||||
|
OidcUser user = delegate.loadUser(userRequest);
|
||||||
|
String username = user.getUserInfo().getClaimAsString(usernameAttribute);
|
||||||
|
Optional<User> duser = userService.findByUsernameIgnoreCase(username);
|
||||||
|
if (duser.isPresent()) {
|
||||||
|
if (loginAttemptService.isBlocked(username)) {
|
||||||
|
throw new LockedException(
|
||||||
|
"Your account has been locked due to too many failed login attempts.");
|
||||||
|
}
|
||||||
|
if (userService.hasPassword(username)) {
|
||||||
|
throw new IllegalArgumentException("Password must not be null");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Return a new OidcUser with adjusted attributes
|
||||||
|
return new DefaultOidcUser(
|
||||||
|
user.getAuthorities(),
|
||||||
|
userRequest.getIdToken(),
|
||||||
|
user.getUserInfo(),
|
||||||
|
usernameAttribute);
|
||||||
|
} catch (java.lang.IllegalArgumentException e) {
|
||||||
|
logger.error("Error loading OIDC user: {}", e.getMessage());
|
||||||
|
throw new OAuth2AuthenticationException(new OAuth2Error(e.getMessage()), e);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("Unexpected error loading OIDC user", e);
|
||||||
|
throw new OAuth2AuthenticationException("Unexpected error during authentication");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package stirling.software.SPDF.config.security.oauth2;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
|
||||||
|
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
|
||||||
|
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
|
||||||
|
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||||
|
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||||
|
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
|
||||||
|
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
|
|
||||||
|
public class CustomOAuthUserService implements OAuth2UserService<OidcUserRequest, OidcUser> {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(CustomOAuthUserService.class);
|
||||||
|
|
||||||
|
private final OidcUserService delegate = new OidcUserService();
|
||||||
|
|
||||||
|
private ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
|
public CustomOAuthUserService(ApplicationProperties applicationProperties) {
|
||||||
|
this.applicationProperties = applicationProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException {
|
||||||
|
String usernameAttribute =
|
||||||
|
applicationProperties.getSecurity().getOAUTH2().getUseAsUsername();
|
||||||
|
try {
|
||||||
|
|
||||||
|
OidcUser user = delegate.loadUser(userRequest);
|
||||||
|
Map<String, Object> attributes = new HashMap<>(user.getAttributes());
|
||||||
|
|
||||||
|
// Ensure the preferred username attribute is present
|
||||||
|
if (!attributes.containsKey(usernameAttribute)) {
|
||||||
|
attributes.put(usernameAttribute, attributes.getOrDefault("email", ""));
|
||||||
|
usernameAttribute = "email";
|
||||||
|
logger.info("Adjusted username attribute to use email");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return a new OidcUser with adjusted attributes
|
||||||
|
return new DefaultOidcUser(
|
||||||
|
user.getAuthorities(),
|
||||||
|
userRequest.getIdToken(),
|
||||||
|
user.getUserInfo(),
|
||||||
|
usernameAttribute);
|
||||||
|
} catch (java.lang.IllegalArgumentException e) {
|
||||||
|
throw new OAuth2AuthenticationException(
|
||||||
|
new OAuth2Error(e.getMessage()), e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,7 +38,7 @@ 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 {
|
public PDDocument mergeDocuments(List<PDDocument> documents) throws IOException {
|
||||||
PDDocument mergedDoc = new PDDocument();
|
PDDocument mergedDoc = new PDDocument();
|
||||||
for (PDDocument doc : documents) {
|
for (PDDocument doc : documents) {
|
||||||
for (PDPage page : doc.getPages()) {
|
for (PDPage page : doc.getPages()) {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
|||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import io.github.pixee.security.Filenames;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
@@ -136,6 +137,7 @@ public class MultiPageLayoutController {
|
|||||||
byte[] result = baos.toByteArray();
|
byte[] result = baos.toByteArray();
|
||||||
return WebResponseUtils.bytesToWebResponse(
|
return WebResponseUtils.bytesToWebResponse(
|
||||||
result,
|
result,
|
||||||
file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_layoutChanged.pdf");
|
Filenames.toSimpleFileName(file.getOriginalFilename()).replaceFirst("[.][^.]+$", "")
|
||||||
|
+ "_layoutChanged.pdf");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package stirling.software.SPDF.controller.api;
|
|||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -19,6 +20,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
|||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import io.github.pixee.security.Filenames;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
@@ -75,7 +77,8 @@ public class PdfOverlayController {
|
|||||||
overlay.overlay(overlayGuide).save(outputStream);
|
overlay.overlay(overlayGuide).save(outputStream);
|
||||||
byte[] data = outputStream.toByteArray();
|
byte[] data = outputStream.toByteArray();
|
||||||
String outputFilename =
|
String outputFilename =
|
||||||
baseFile.getOriginalFilename().replaceFirst("[.][^.]+$", "")
|
Filenames.toSimpleFileName(baseFile.getOriginalFilename())
|
||||||
|
.replaceFirst("[.][^.]+$", "")
|
||||||
+ "_overlayed.pdf"; // Remove file extension and append .pdf
|
+ "_overlayed.pdf"; // Remove file extension and append .pdf
|
||||||
|
|
||||||
return WebResponseUtils.bytesToWebResponse(
|
return WebResponseUtils.bytesToWebResponse(
|
||||||
@@ -135,7 +138,7 @@ public class PdfOverlayController {
|
|||||||
try (PDDocument overlayPdf = Loader.loadPDF(overlayFiles[overlayFileIndex])) {
|
try (PDDocument overlayPdf = Loader.loadPDF(overlayFiles[overlayFileIndex])) {
|
||||||
PDDocument singlePageDocument = new PDDocument();
|
PDDocument singlePageDocument = new PDDocument();
|
||||||
singlePageDocument.addPage(overlayPdf.getPage(pageCountInCurrentOverlay));
|
singlePageDocument.addPage(overlayPdf.getPage(pageCountInCurrentOverlay));
|
||||||
File tempFile = File.createTempFile("overlay-page-", ".pdf");
|
File tempFile = Files.createTempFile("overlay-page-", ".pdf").toFile();
|
||||||
singlePageDocument.save(tempFile);
|
singlePageDocument.save(tempFile);
|
||||||
singlePageDocument.close();
|
singlePageDocument.close();
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package stirling.software.SPDF.controller.api;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import org.apache.pdfbox.Loader;
|
import org.apache.pdfbox.Loader;
|
||||||
@@ -16,6 +17,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
|||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import io.github.pixee.security.Filenames;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
@@ -49,7 +51,9 @@ public class RearrangePagesPDFController {
|
|||||||
String[] pageOrderArr = pagesToDelete.split(",");
|
String[] pageOrderArr = pagesToDelete.split(",");
|
||||||
|
|
||||||
List<Integer> pagesToRemove =
|
List<Integer> pagesToRemove =
|
||||||
GeneralUtils.parsePageList(pageOrderArr, document.getNumberOfPages());
|
GeneralUtils.parsePageList(pageOrderArr, document.getNumberOfPages(), false);
|
||||||
|
|
||||||
|
Collections.sort(pagesToRemove);
|
||||||
|
|
||||||
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);
|
||||||
@@ -57,7 +61,9 @@ public class RearrangePagesPDFController {
|
|||||||
}
|
}
|
||||||
return WebResponseUtils.pdfDocToWebResponse(
|
return WebResponseUtils.pdfDocToWebResponse(
|
||||||
document,
|
document,
|
||||||
pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_removed_pages.pdf");
|
Filenames.toSimpleFileName(pdfFile.getOriginalFilename())
|
||||||
|
.replaceFirst("[.][^.]+$", "")
|
||||||
|
+ "_removed_pages.pdf");
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<Integer> removeFirst(int totalPages) {
|
private List<Integer> removeFirst(int totalPages) {
|
||||||
@@ -189,7 +195,7 @@ public class RearrangePagesPDFController {
|
|||||||
if (sortType != null && sortType.length() > 0) {
|
if (sortType != null && sortType.length() > 0) {
|
||||||
newPageOrder = processSortTypes(sortType, totalPages);
|
newPageOrder = processSortTypes(sortType, totalPages);
|
||||||
} else {
|
} else {
|
||||||
newPageOrder = GeneralUtils.parsePageList(pageOrderArr, totalPages);
|
newPageOrder = GeneralUtils.parsePageList(pageOrderArr, totalPages, false);
|
||||||
}
|
}
|
||||||
logger.info("newPageOrder = " + newPageOrder);
|
logger.info("newPageOrder = " + newPageOrder);
|
||||||
logger.info("totalPages = " + totalPages);
|
logger.info("totalPages = " + totalPages);
|
||||||
@@ -211,7 +217,8 @@ public class RearrangePagesPDFController {
|
|||||||
|
|
||||||
return WebResponseUtils.pdfDocToWebResponse(
|
return WebResponseUtils.pdfDocToWebResponse(
|
||||||
document,
|
document,
|
||||||
pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "")
|
Filenames.toSimpleFileName(pdfFile.getOriginalFilename())
|
||||||
|
.replaceFirst("[.][^.]+$", "")
|
||||||
+ "_rearranged.pdf");
|
+ "_rearranged.pdf");
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
logger.error("Failed rearranging documents", e);
|
logger.error("Failed rearranging documents", e);
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
|||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import io.github.pixee.security.Filenames;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
@@ -49,6 +50,8 @@ public class RotationController {
|
|||||||
|
|
||||||
return WebResponseUtils.pdfDocToWebResponse(
|
return WebResponseUtils.pdfDocToWebResponse(
|
||||||
document,
|
document,
|
||||||
pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_rotated.pdf");
|
Filenames.toSimpleFileName(pdfFile.getOriginalFilename())
|
||||||
|
.replaceFirst("[.][^.]+$", "")
|
||||||
|
+ "_rotated.pdf");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
|||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import io.github.pixee.security.Filenames;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
@@ -112,6 +113,7 @@ public class ScalePagesController {
|
|||||||
|
|
||||||
return WebResponseUtils.bytesToWebResponse(
|
return WebResponseUtils.bytesToWebResponse(
|
||||||
baos.toByteArray(),
|
baos.toByteArray(),
|
||||||
file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_scaled.pdf");
|
Filenames.toSimpleFileName(file.getOriginalFilename()).replaceFirst("[.][^.]+$", "")
|
||||||
|
+ "_scaled.pdf");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,10 +23,13 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
|||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import io.github.pixee.security.Filenames;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.model.PdfMetadata;
|
||||||
import stirling.software.SPDF.model.api.PDFWithPageNums;
|
import stirling.software.SPDF.model.api.PDFWithPageNums;
|
||||||
|
import stirling.software.SPDF.utils.PdfUtils;
|
||||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@@ -48,10 +51,17 @@ public class SplitPDFController {
|
|||||||
// open the pdf document
|
// open the pdf document
|
||||||
|
|
||||||
PDDocument document = Loader.loadPDF(file.getBytes());
|
PDDocument document = Loader.loadPDF(file.getBytes());
|
||||||
|
PdfMetadata metadata = PdfUtils.extractMetadataFromPdf(document);
|
||||||
|
int totalPages = document.getNumberOfPages();
|
||||||
|
List<Integer> pageNumbers = request.getPageNumbersList(document, false);
|
||||||
|
System.out.println(
|
||||||
|
pageNumbers.stream().map(String::valueOf).collect(Collectors.joining(",")));
|
||||||
|
if (!pageNumbers.contains(totalPages - 1)) {
|
||||||
|
// Create a mutable ArrayList so we can add to it
|
||||||
|
pageNumbers = new ArrayList<>(pageNumbers);
|
||||||
|
pageNumbers.add(totalPages - 1);
|
||||||
|
}
|
||||||
|
|
||||||
List<Integer> pageNumbers = request.getPageNumbersList(document);
|
|
||||||
if (!pageNumbers.contains(document.getNumberOfPages() - 1))
|
|
||||||
pageNumbers.add(document.getNumberOfPages() - 1);
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"Splitting PDF into pages: {}",
|
"Splitting PDF into pages: {}",
|
||||||
pageNumbers.stream().map(String::valueOf).collect(Collectors.joining(",")));
|
pageNumbers.stream().map(String::valueOf).collect(Collectors.joining(",")));
|
||||||
@@ -64,10 +74,13 @@ public class SplitPDFController {
|
|||||||
for (int i = previousPageNumber; i <= splitPoint; 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.info("Adding page {} to split document", i);
|
||||||
}
|
}
|
||||||
previousPageNumber = splitPoint + 1;
|
previousPageNumber = splitPoint + 1;
|
||||||
|
|
||||||
|
// Transfer metadata to split pdf
|
||||||
|
PdfUtils.setMetadataToPdf(splitDocument, metadata);
|
||||||
|
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
splitDocument.save(baos);
|
splitDocument.save(baos);
|
||||||
|
|
||||||
@@ -83,7 +96,9 @@ public class SplitPDFController {
|
|||||||
|
|
||||||
Path zipFile = Files.createTempFile("split_documents", ".zip");
|
Path zipFile = Files.createTempFile("split_documents", ".zip");
|
||||||
|
|
||||||
String filename = file.getOriginalFilename().replaceFirst("[.][^.]+$", "");
|
String filename =
|
||||||
|
Filenames.toSimpleFileName(file.getOriginalFilename())
|
||||||
|
.replaceFirst("[.][^.]+$", "");
|
||||||
try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(zipFile))) {
|
try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(zipFile))) {
|
||||||
// loop through the split documents and write them to the zip file
|
// loop through the split documents and write them to the zip file
|
||||||
for (int i = 0; i < splitDocumentsBoas.size(); i++) {
|
for (int i = 0; i < splitDocumentsBoas.size(); i++) {
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
|||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import io.github.pixee.security.Filenames;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
@@ -52,8 +53,21 @@ public class SplitPdfBySectionsController {
|
|||||||
// Process the PDF based on split parameters
|
// Process the PDF based on split parameters
|
||||||
int horiz = request.getHorizontalDivisions() + 1;
|
int horiz = request.getHorizontalDivisions() + 1;
|
||||||
int verti = request.getVerticalDivisions() + 1;
|
int verti = request.getVerticalDivisions() + 1;
|
||||||
|
boolean merge = request.isMerge();
|
||||||
List<PDDocument> splitDocuments = splitPdfPages(sourceDocument, verti, horiz);
|
List<PDDocument> splitDocuments = splitPdfPages(sourceDocument, verti, horiz);
|
||||||
|
|
||||||
|
String filename =
|
||||||
|
Filenames.toSimpleFileName(file.getOriginalFilename())
|
||||||
|
.replaceFirst("[.][^.]+$", "");
|
||||||
|
if (merge) {
|
||||||
|
MergeController mergeController = new MergeController();
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
mergeController.mergeDocuments(splitDocuments).save(baos);
|
||||||
|
return WebResponseUtils.bytesToWebResponse(
|
||||||
|
baos.toByteArray(),
|
||||||
|
filename + "_split.pdf",
|
||||||
|
MediaType.APPLICATION_OCTET_STREAM);
|
||||||
|
}
|
||||||
for (PDDocument doc : splitDocuments) {
|
for (PDDocument doc : splitDocuments) {
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
doc.save(baos);
|
doc.save(baos);
|
||||||
@@ -64,7 +78,6 @@ public class SplitPdfBySectionsController {
|
|||||||
sourceDocument.close();
|
sourceDocument.close();
|
||||||
|
|
||||||
Path zipFile = Files.createTempFile("split_documents", ".zip");
|
Path zipFile = Files.createTempFile("split_documents", ".zip");
|
||||||
String filename = file.getOriginalFilename().replaceFirst("[.][^.]+$", "");
|
|
||||||
byte[] data;
|
byte[] data;
|
||||||
|
|
||||||
try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(zipFile))) {
|
try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(zipFile))) {
|
||||||
@@ -121,8 +134,8 @@ public class SplitPdfBySectionsController {
|
|||||||
subDoc, subPage, AppendMode.APPEND, true, true)) {
|
subDoc, subPage, AppendMode.APPEND, true, true)) {
|
||||||
// Set clipping area and position
|
// Set clipping area and position
|
||||||
float translateX = -subPageWidth * i;
|
float translateX = -subPageWidth * i;
|
||||||
|
|
||||||
//float translateY = height - subPageHeight * (verticalDivisions - j);
|
// float translateY = height - subPageHeight * (verticalDivisions - j);
|
||||||
float translateY = -subPageHeight * (verticalDivisions - 1 - j);
|
float translateY = -subPageHeight * (verticalDivisions - 1 - j);
|
||||||
|
|
||||||
contentStream.saveGraphicsState();
|
contentStream.saveGraphicsState();
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ import java.io.ByteArrayOutputStream;
|
|||||||
import java.io.IOException;
|
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.List;
|
|
||||||
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipEntry;
|
||||||
import java.util.zip.ZipOutputStream;
|
import java.util.zip.ZipOutputStream;
|
||||||
|
|
||||||
@@ -20,6 +18,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
|||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import io.github.pixee.security.Filenames;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
@@ -40,115 +39,137 @@ public class SplitPdfBySizeController {
|
|||||||
+ " 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")
|
+ " 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)
|
public ResponseEntity<byte[]> autoSplitPdf(@ModelAttribute SplitPdfBySizeOrCountRequest request)
|
||||||
throws Exception {
|
throws Exception {
|
||||||
List<ByteArrayOutputStream> splitDocumentsBoas = new ArrayList<ByteArrayOutputStream>();
|
|
||||||
|
|
||||||
MultipartFile file = request.getFileInput();
|
MultipartFile file = request.getFileInput();
|
||||||
PDDocument sourceDocument = Loader.loadPDF(file.getBytes());
|
|
||||||
|
|
||||||
// 0 = size, 1 = page count, 2 = doc count
|
|
||||||
int type = request.getSplitType();
|
|
||||||
String value = request.getSplitValue();
|
|
||||||
|
|
||||||
if (type == 0) { // Split by size
|
|
||||||
long maxBytes = GeneralUtils.convertSizeToBytes(value);
|
|
||||||
long currentSize = 0;
|
|
||||||
PDDocument currentDoc = new PDDocument();
|
|
||||||
|
|
||||||
for (PDPage page : sourceDocument.getPages()) {
|
|
||||||
ByteArrayOutputStream pageOutputStream = new ByteArrayOutputStream();
|
|
||||||
PDDocument tempDoc = new PDDocument();
|
|
||||||
tempDoc.addPage(page);
|
|
||||||
tempDoc.save(pageOutputStream);
|
|
||||||
tempDoc.close();
|
|
||||||
|
|
||||||
long pageSize = pageOutputStream.size();
|
|
||||||
if (currentSize + pageSize > maxBytes) {
|
|
||||||
// Save and reset current document
|
|
||||||
splitDocumentsBoas.add(currentDocToByteArray(currentDoc));
|
|
||||||
currentDoc = new PDDocument();
|
|
||||||
currentSize = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
currentDoc.addPage(page);
|
|
||||||
currentSize += pageSize;
|
|
||||||
}
|
|
||||||
// Add the last document if it contains any pages
|
|
||||||
if (currentDoc.getPages().getCount() != 0) {
|
|
||||||
splitDocumentsBoas.add(currentDocToByteArray(currentDoc));
|
|
||||||
}
|
|
||||||
} else if (type == 1) { // Split by page count
|
|
||||||
int pageCount = Integer.parseInt(value);
|
|
||||||
int currentPageCount = 0;
|
|
||||||
PDDocument currentDoc = new PDDocument();
|
|
||||||
|
|
||||||
for (PDPage page : sourceDocument.getPages()) {
|
|
||||||
currentDoc.addPage(page);
|
|
||||||
currentPageCount++;
|
|
||||||
|
|
||||||
if (currentPageCount == pageCount) {
|
|
||||||
// Save and reset current document
|
|
||||||
splitDocumentsBoas.add(currentDocToByteArray(currentDoc));
|
|
||||||
currentDoc = new PDDocument();
|
|
||||||
currentPageCount = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Add the last document if it contains any pages
|
|
||||||
if (currentDoc.getPages().getCount() != 0) {
|
|
||||||
splitDocumentsBoas.add(currentDocToByteArray(currentDoc));
|
|
||||||
}
|
|
||||||
} else if (type == 2) { // Split by doc count
|
|
||||||
int documentCount = Integer.parseInt(value);
|
|
||||||
int totalPageCount = sourceDocument.getNumberOfPages();
|
|
||||||
int pagesPerDocument = totalPageCount / documentCount;
|
|
||||||
int extraPages = totalPageCount % documentCount;
|
|
||||||
int currentPageIndex = 0;
|
|
||||||
|
|
||||||
for (int i = 0; i < documentCount; i++) {
|
|
||||||
PDDocument currentDoc = new PDDocument();
|
|
||||||
int pagesToAdd = pagesPerDocument + (i < extraPages ? 1 : 0);
|
|
||||||
|
|
||||||
for (int j = 0; j < pagesToAdd; j++) {
|
|
||||||
currentDoc.addPage(sourceDocument.getPage(currentPageIndex++));
|
|
||||||
}
|
|
||||||
|
|
||||||
splitDocumentsBoas.add(currentDocToByteArray(currentDoc));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new IllegalArgumentException("Invalid argument for split type");
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceDocument.close();
|
|
||||||
|
|
||||||
Path zipFile = Files.createTempFile("split_documents", ".zip");
|
Path zipFile = Files.createTempFile("split_documents", ".zip");
|
||||||
String filename = file.getOriginalFilename().replaceFirst("[.][^.]+$", "");
|
String filename =
|
||||||
byte[] data;
|
Filenames.toSimpleFileName(file.getOriginalFilename())
|
||||||
|
.replaceFirst("[.][^.]+$", "");
|
||||||
|
byte[] data = null;
|
||||||
|
try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(zipFile));
|
||||||
|
PDDocument sourceDocument = Loader.loadPDF(file.getBytes())) {
|
||||||
|
|
||||||
try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(zipFile))) {
|
int type = request.getSplitType();
|
||||||
for (int i = 0; i < splitDocumentsBoas.size(); i++) {
|
String value = request.getSplitValue();
|
||||||
String fileName = filename + "_" + (i + 1) + ".pdf";
|
|
||||||
ByteArrayOutputStream baos = splitDocumentsBoas.get(i);
|
|
||||||
byte[] pdf = baos.toByteArray();
|
|
||||||
|
|
||||||
ZipEntry pdfEntry = new ZipEntry(fileName);
|
if (type == 0) {
|
||||||
zipOut.putNextEntry(pdfEntry);
|
long maxBytes = GeneralUtils.convertSizeToBytes(value);
|
||||||
zipOut.write(pdf);
|
handleSplitBySize(sourceDocument, maxBytes, zipOut, filename);
|
||||||
zipOut.closeEntry();
|
} else if (type == 1) {
|
||||||
|
int pageCount = Integer.parseInt(value);
|
||||||
|
handleSplitByPageCount(sourceDocument, pageCount, zipOut, filename);
|
||||||
|
} else if (type == 2) {
|
||||||
|
int documentCount = Integer.parseInt(value);
|
||||||
|
handleSplitByDocCount(sourceDocument, documentCount, zipOut, filename);
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException("Invalid argument for split type");
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
} finally {
|
} finally {
|
||||||
data = Files.readAllBytes(zipFile);
|
data = Files.readAllBytes(zipFile);
|
||||||
Files.delete(zipFile);
|
Files.deleteIfExists(zipFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
return WebResponseUtils.bytesToWebResponse(
|
return WebResponseUtils.bytesToWebResponse(
|
||||||
data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM);
|
data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ByteArrayOutputStream currentDocToByteArray(PDDocument document) throws IOException {
|
private void handleSplitBySize(
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
PDDocument sourceDocument, long maxBytes, ZipOutputStream zipOut, String baseFilename)
|
||||||
document.save(baos);
|
throws IOException {
|
||||||
document.close();
|
long currentSize = 0;
|
||||||
return baos;
|
PDDocument currentDoc = new PDDocument();
|
||||||
|
int fileIndex = 1;
|
||||||
|
|
||||||
|
for (int pageIndex = 0; pageIndex < sourceDocument.getNumberOfPages(); pageIndex++) {
|
||||||
|
PDPage page = sourceDocument.getPage(pageIndex);
|
||||||
|
ByteArrayOutputStream pageOutputStream = new ByteArrayOutputStream();
|
||||||
|
|
||||||
|
try (PDDocument tempDoc = new PDDocument()) {
|
||||||
|
PDPage importedPage = tempDoc.importPage(page); // This creates a new PDPage object
|
||||||
|
tempDoc.save(pageOutputStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
long pageSize = pageOutputStream.size();
|
||||||
|
if (currentSize + pageSize > maxBytes) {
|
||||||
|
if (currentDoc.getNumberOfPages() > 0) {
|
||||||
|
saveDocumentToZip(currentDoc, zipOut, baseFilename, fileIndex++);
|
||||||
|
currentDoc.close(); // Make sure to close the document
|
||||||
|
currentDoc = new PDDocument();
|
||||||
|
currentSize = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PDPage newPage = new PDPage(page.getCOSObject()); // Re-create the page
|
||||||
|
currentDoc.addPage(newPage);
|
||||||
|
currentSize += pageSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentDoc.getNumberOfPages() != 0) {
|
||||||
|
saveDocumentToZip(currentDoc, zipOut, baseFilename, fileIndex++);
|
||||||
|
currentDoc.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleSplitByPageCount(
|
||||||
|
PDDocument sourceDocument, int pageCount, ZipOutputStream zipOut, String baseFilename)
|
||||||
|
throws IOException {
|
||||||
|
int currentPageCount = 0;
|
||||||
|
PDDocument currentDoc = new PDDocument();
|
||||||
|
int fileIndex = 1;
|
||||||
|
for (PDPage page : sourceDocument.getPages()) {
|
||||||
|
currentDoc.addPage(page);
|
||||||
|
currentPageCount++;
|
||||||
|
|
||||||
|
if (currentPageCount == pageCount) {
|
||||||
|
// Save and reset current document
|
||||||
|
saveDocumentToZip(currentDoc, zipOut, baseFilename, fileIndex++);
|
||||||
|
currentDoc = new PDDocument();
|
||||||
|
currentPageCount = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Add the last document if it contains any pages
|
||||||
|
if (currentDoc.getPages().getCount() != 0) {
|
||||||
|
saveDocumentToZip(currentDoc, zipOut, baseFilename, fileIndex++);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleSplitByDocCount(
|
||||||
|
PDDocument sourceDocument,
|
||||||
|
int documentCount,
|
||||||
|
ZipOutputStream zipOut,
|
||||||
|
String baseFilename)
|
||||||
|
throws IOException {
|
||||||
|
int totalPageCount = sourceDocument.getNumberOfPages();
|
||||||
|
int pagesPerDocument = totalPageCount / documentCount;
|
||||||
|
int extraPages = totalPageCount % documentCount;
|
||||||
|
int currentPageIndex = 0;
|
||||||
|
int fileIndex = 1;
|
||||||
|
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++));
|
||||||
|
}
|
||||||
|
|
||||||
|
saveDocumentToZip(currentDoc, zipOut, baseFilename, fileIndex++);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveDocumentToZip(
|
||||||
|
PDDocument document, ZipOutputStream zipOut, String baseFilename, int index)
|
||||||
|
throws IOException {
|
||||||
|
ByteArrayOutputStream outStream = new ByteArrayOutputStream();
|
||||||
|
document.save(outStream);
|
||||||
|
document.close(); // Close the document to free resources
|
||||||
|
|
||||||
|
// Create a new zip entry
|
||||||
|
ZipEntry zipEntry = new ZipEntry(baseFilename + "_" + index + ".pdf");
|
||||||
|
zipOut.putNextEntry(zipEntry);
|
||||||
|
zipOut.write(outStream.toByteArray());
|
||||||
|
zipOut.closeEntry();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ import org.springframework.http.HttpStatus;
|
|||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.session.SessionInformation;
|
||||||
|
import org.springframework.security.core.session.SessionRegistry;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
|
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.ui.Model;
|
import org.springframework.ui.Model;
|
||||||
@@ -28,7 +31,6 @@ import jakarta.servlet.http.HttpServletResponse;
|
|||||||
import stirling.software.SPDF.config.security.UserService;
|
import stirling.software.SPDF.config.security.UserService;
|
||||||
import stirling.software.SPDF.model.Role;
|
import stirling.software.SPDF.model.Role;
|
||||||
import stirling.software.SPDF.model.User;
|
import stirling.software.SPDF.model.User;
|
||||||
import stirling.software.SPDF.model.api.user.UpdateUserDetails;
|
|
||||||
import stirling.software.SPDF.model.api.user.UsernameAndPass;
|
import stirling.software.SPDF.model.api.user.UsernameAndPass;
|
||||||
|
|
||||||
@Controller
|
@Controller
|
||||||
@@ -41,33 +43,85 @@ public class UserController {
|
|||||||
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
||||||
@PostMapping("/register")
|
@PostMapping("/register")
|
||||||
public String register(@ModelAttribute UsernameAndPass requestModel, Model model) {
|
public String register(@ModelAttribute UsernameAndPass requestModel, Model model) {
|
||||||
if (userService.usernameExists(requestModel.getUsername())) {
|
if (userService.usernameExistsIgnoreCase(requestModel.getUsername())) {
|
||||||
model.addAttribute("error", "Username already exists");
|
model.addAttribute("error", "Username already exists");
|
||||||
return "register";
|
return "register";
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
userService.saveUser(requestModel.getUsername(), requestModel.getPassword());
|
userService.saveUser(requestModel.getUsername(), requestModel.getPassword());
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
return "redirect:/login?messageType=invalidUsername";
|
||||||
|
}
|
||||||
return "redirect:/login?registered=true";
|
return "redirect:/login?registered=true";
|
||||||
}
|
}
|
||||||
|
|
||||||
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
||||||
@PostMapping("/change-username-and-password")
|
@PostMapping("/change-username")
|
||||||
public RedirectView changeUsernameAndPassword(
|
public RedirectView changeUsername(
|
||||||
Principal principal,
|
Principal principal,
|
||||||
@ModelAttribute UpdateUserDetails requestModel,
|
@RequestParam(name = "currentPassword") String currentPassword,
|
||||||
|
@RequestParam(name = "newUsername") String newUsername,
|
||||||
HttpServletRequest request,
|
HttpServletRequest request,
|
||||||
HttpServletResponse response,
|
HttpServletResponse response,
|
||||||
RedirectAttributes redirectAttributes) {
|
RedirectAttributes redirectAttributes) {
|
||||||
|
|
||||||
String currentPassword = requestModel.getPassword();
|
if (!userService.isUsernameValid(newUsername)) {
|
||||||
String newPassword = requestModel.getNewPassword();
|
return new RedirectView("/account?messageType=invalidUsername");
|
||||||
String newUsername = requestModel.getNewUsername();
|
}
|
||||||
|
|
||||||
|
if (principal == null) {
|
||||||
|
return new RedirectView("/account?messageType=notAuthenticated");
|
||||||
|
}
|
||||||
|
|
||||||
|
// The username MUST be unique when renaming
|
||||||
|
Optional<User> userOpt = userService.findByUsername(principal.getName());
|
||||||
|
|
||||||
|
if (userOpt == null || userOpt.isEmpty()) {
|
||||||
|
return new RedirectView("/account?messageType=userNotFound");
|
||||||
|
}
|
||||||
|
|
||||||
|
User user = userOpt.get();
|
||||||
|
|
||||||
|
if (user.getUsername().equals(newUsername)) {
|
||||||
|
return new RedirectView("/account?messageType=usernameExists");
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
try {
|
||||||
|
userService.changeUsername(user, newUsername);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
return new RedirectView("/account?messageType=invalidUsername");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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-on-login")
|
||||||
|
public RedirectView changePasswordOnLogin(
|
||||||
|
Principal principal,
|
||||||
|
@RequestParam(name = "currentPassword") String currentPassword,
|
||||||
|
@RequestParam(name = "newPassword") String newPassword,
|
||||||
|
HttpServletRequest request,
|
||||||
|
HttpServletResponse response,
|
||||||
|
RedirectAttributes redirectAttributes) {
|
||||||
if (principal == null) {
|
if (principal == null) {
|
||||||
return new RedirectView("/change-creds?messageType=notAuthenticated");
|
return new RedirectView("/change-creds?messageType=notAuthenticated");
|
||||||
}
|
}
|
||||||
|
|
||||||
Optional<User> userOpt = userService.findByUsername(principal.getName());
|
Optional<User> userOpt = userService.findByUsernameIgnoreCase(principal.getName());
|
||||||
|
|
||||||
if (userOpt == null || userOpt.isEmpty()) {
|
if (userOpt == null || userOpt.isEmpty()) {
|
||||||
return new RedirectView("/change-creds?messageType=userNotFound");
|
return new RedirectView("/change-creds?messageType=userNotFound");
|
||||||
@@ -79,69 +133,20 @@ public class UserController {
|
|||||||
return new RedirectView("/change-creds?messageType=incorrectPassword");
|
return new RedirectView("/change-creds?messageType=incorrectPassword");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user.getUsername().equals(newUsername) && userService.usernameExists(newUsername)) {
|
|
||||||
return new RedirectView("/change-creds?messageType=usernameExists");
|
|
||||||
}
|
|
||||||
|
|
||||||
userService.changePassword(user, newPassword);
|
userService.changePassword(user, newPassword);
|
||||||
if (newUsername != null
|
|
||||||
&& newUsername.length() > 0
|
|
||||||
&& !user.getUsername().equals(newUsername)) {
|
|
||||||
userService.changeUsername(user, newUsername);
|
|
||||||
}
|
|
||||||
userService.changeFirstUse(user, false);
|
userService.changeFirstUse(user, false);
|
||||||
|
|
||||||
// Logout using Spring's utility
|
// Logout using Spring's utility
|
||||||
new SecurityContextLogoutHandler().logout(request, response, null);
|
new SecurityContextLogoutHandler().logout(request, response, null);
|
||||||
|
|
||||||
return new RedirectView("/login?messageType=credsUpdated");
|
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')")
|
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
||||||
@PostMapping("/change-password")
|
@PostMapping("/change-password")
|
||||||
public RedirectView changePassword(
|
public RedirectView changePassword(
|
||||||
Principal principal,
|
Principal principal,
|
||||||
@RequestParam String currentPassword,
|
@RequestParam(name = "currentPassword") String currentPassword,
|
||||||
@RequestParam String newPassword,
|
@RequestParam(name = "newPassword") String newPassword,
|
||||||
HttpServletRequest request,
|
HttpServletRequest request,
|
||||||
HttpServletResponse response,
|
HttpServletResponse response,
|
||||||
RedirectAttributes redirectAttributes) {
|
RedirectAttributes redirectAttributes) {
|
||||||
@@ -149,7 +154,7 @@ public class UserController {
|
|||||||
return new RedirectView("/account?messageType=notAuthenticated");
|
return new RedirectView("/account?messageType=notAuthenticated");
|
||||||
}
|
}
|
||||||
|
|
||||||
Optional<User> userOpt = userService.findByUsername(principal.getName());
|
Optional<User> userOpt = userService.findByUsernameIgnoreCase(principal.getName());
|
||||||
|
|
||||||
if (userOpt == null || userOpt.isEmpty()) {
|
if (userOpt == null || userOpt.isEmpty()) {
|
||||||
return new RedirectView("/account?messageType=userNotFound");
|
return new RedirectView("/account?messageType=userNotFound");
|
||||||
@@ -166,7 +171,7 @@ public class UserController {
|
|||||||
// Logout using Spring's utility
|
// Logout using Spring's utility
|
||||||
new SecurityContextLogoutHandler().logout(request, response, null);
|
new SecurityContextLogoutHandler().logout(request, response, null);
|
||||||
|
|
||||||
return new RedirectView("/login?messageType=credsUpdated");
|
return new RedirectView(LOGIN_MESSAGETYPE_CREDSUPDATED);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
||||||
@@ -192,13 +197,25 @@ public class UserController {
|
|||||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||||
@PostMapping("/admin/saveUser")
|
@PostMapping("/admin/saveUser")
|
||||||
public RedirectView saveUser(
|
public RedirectView saveUser(
|
||||||
@RequestParam String username,
|
@RequestParam(name = "username") String username,
|
||||||
@RequestParam String password,
|
@RequestParam(name = "password") String password,
|
||||||
@RequestParam String role,
|
@RequestParam(name = "role") String role,
|
||||||
@RequestParam(name = "forceChange", required = false, defaultValue = "false")
|
@RequestParam(name = "forceChange", required = false, defaultValue = "false")
|
||||||
boolean forceChange) {
|
boolean forceChange) {
|
||||||
|
|
||||||
if (userService.usernameExists(username)) {
|
if (!userService.isUsernameValid(username)) {
|
||||||
|
return new RedirectView("/addUsers?messageType=invalidUsername");
|
||||||
|
}
|
||||||
|
|
||||||
|
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
|
||||||
|
|
||||||
|
if (userOpt.isPresent()) {
|
||||||
|
User user = userOpt.get();
|
||||||
|
if (user != null && user.getUsername().equalsIgnoreCase(username)) {
|
||||||
|
return new RedirectView("/addUsers?messageType=usernameExists");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (userService.usernameExistsIgnoreCase(username)) {
|
||||||
return new RedirectView("/addUsers?messageType=usernameExists");
|
return new RedirectView("/addUsers?messageType=usernameExists");
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -217,20 +234,80 @@ public class UserController {
|
|||||||
return new RedirectView("/addUsers"); // Redirect to account page after adding the user
|
return new RedirectView("/addUsers"); // Redirect to account page after adding the user
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||||
|
@PostMapping("/admin/changeRole")
|
||||||
|
public RedirectView changeRole(
|
||||||
|
@RequestParam(name = "username") String username,
|
||||||
|
@RequestParam(name = "role") String role,
|
||||||
|
Authentication authentication) {
|
||||||
|
|
||||||
|
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
|
||||||
|
|
||||||
|
if (!userOpt.isPresent()) {
|
||||||
|
return new RedirectView("/addUsers?messageType=userNotFound");
|
||||||
|
}
|
||||||
|
if (!userService.usernameExistsIgnoreCase(username)) {
|
||||||
|
return new RedirectView("/addUsers?messageType=userNotFound");
|
||||||
|
}
|
||||||
|
// Get the currently authenticated username
|
||||||
|
String currentUsername = authentication.getName();
|
||||||
|
|
||||||
|
// Check if the provided username matches the current session's username
|
||||||
|
if (currentUsername.equalsIgnoreCase(username)) {
|
||||||
|
return new RedirectView("/addUsers?messageType=downgradeCurrentUser");
|
||||||
|
}
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
User user = userOpt.get();
|
||||||
|
|
||||||
|
userService.changeRole(user, role);
|
||||||
|
return new RedirectView("/addUsers"); // Redirect to account page after adding the user
|
||||||
|
}
|
||||||
|
|
||||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||||
@PostMapping("/admin/deleteUser/{username}")
|
@PostMapping("/admin/deleteUser/{username}")
|
||||||
public String deleteUser(@PathVariable String username, Authentication authentication) {
|
public RedirectView deleteUser(
|
||||||
|
@PathVariable(name = "username") String username, Authentication authentication) {
|
||||||
|
|
||||||
|
if (!userService.usernameExistsIgnoreCase(username)) {
|
||||||
|
return new RedirectView("/addUsers?messageType=deleteUsernameExists");
|
||||||
|
}
|
||||||
|
|
||||||
// Get the currently authenticated username
|
// Get the currently authenticated username
|
||||||
String currentUsername = authentication.getName();
|
String currentUsername = authentication.getName();
|
||||||
|
|
||||||
// Check if the provided username matches the current session's username
|
// Check if the provided username matches the current session's username
|
||||||
if (currentUsername.equals(username)) {
|
if (currentUsername.equalsIgnoreCase(username)) {
|
||||||
throw new IllegalArgumentException("Cannot delete currently logined in user.");
|
return new RedirectView("/addUsers?messageType=deleteCurrentUser");
|
||||||
}
|
}
|
||||||
|
invalidateUserSessions(username);
|
||||||
userService.deleteUser(username);
|
userService.deleteUser(username);
|
||||||
return "redirect:/addUsers";
|
return new RedirectView("/addUsers");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Autowired private SessionRegistry sessionRegistry;
|
||||||
|
|
||||||
|
private void invalidateUserSessions(String username) {
|
||||||
|
for (Object principal : sessionRegistry.getAllPrincipals()) {
|
||||||
|
if (principal instanceof UserDetails) {
|
||||||
|
UserDetails userDetails = (UserDetails) principal;
|
||||||
|
if (userDetails.getUsername().equals(username)) {
|
||||||
|
for (SessionInformation session :
|
||||||
|
sessionRegistry.getAllSessions(principal, false)) {
|
||||||
|
session.expireNow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
||||||
@@ -261,4 +338,6 @@ public class UserController {
|
|||||||
}
|
}
|
||||||
return ResponseEntity.ok(apiKey);
|
return ResponseEntity.ok(apiKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final String LOGIN_MESSAGETYPE_CREDSUPDATED = "/login?messageType=credsUpdated";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
|||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import io.github.pixee.security.Filenames;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
@@ -22,28 +23,28 @@ import stirling.software.SPDF.utils.WebResponseUtils;
|
|||||||
public class ConvertBookToPDFController {
|
public class ConvertBookToPDFController {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
@Qualifier("bookFormatsInstalled")
|
@Qualifier("bookAndHtmlFormatsInstalled")
|
||||||
private boolean bookFormatsInstalled;
|
private boolean bookAndHtmlFormatsInstalled;
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/book/pdf")
|
@PostMapping(consumes = "multipart/form-data", value = "/book/pdf")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary =
|
summary =
|
||||||
"Convert a BOOK/comic (*.epub | *.mobi | *.azw3 | *.fb2 | *.txt | *.docx) to PDF",
|
"Convert a BOOK/comic (*.epub | *.mobi | *.azw3 | *.fb2 | *.txt | *.docx) to PDF",
|
||||||
description =
|
description =
|
||||||
"(Requires bookFormatsInstalled flag and Calibre installed) This endpoint takes an BOOK/comic (*.epub | *.mobi | *.azw3 | *.fb2 | *.txt | *.docx) input and converts it to PDF format.")
|
"(Requires bookAndHtmlFormatsInstalled flag and Calibre installed) This endpoint takes an BOOK/comic (*.epub | *.mobi | *.azw3 | *.fb2 | *.txt | *.docx) input and converts it to PDF format.")
|
||||||
public ResponseEntity<byte[]> HtmlToPdf(@ModelAttribute GeneralFile request) throws Exception {
|
public ResponseEntity<byte[]> HtmlToPdf(@ModelAttribute GeneralFile request) throws Exception {
|
||||||
MultipartFile fileInput = request.getFileInput();
|
MultipartFile fileInput = request.getFileInput();
|
||||||
|
|
||||||
if (!bookFormatsInstalled) {
|
if (!bookAndHtmlFormatsInstalled) {
|
||||||
throw new IllegalArgumentException(
|
throw new IllegalArgumentException(
|
||||||
"bookFormatsInstalled flag is False, this functionality is not avaiable");
|
"bookAndHtmlFormatsInstalled flag is False, this functionality is not available");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fileInput == null) {
|
if (fileInput == null) {
|
||||||
throw new IllegalArgumentException("Please provide a file for conversion.");
|
throw new IllegalArgumentException("Please provide a file for conversion.");
|
||||||
}
|
}
|
||||||
|
|
||||||
String originalFilename = fileInput.getOriginalFilename();
|
String originalFilename = Filenames.toSimpleFileName(fileInput.getOriginalFilename());
|
||||||
|
|
||||||
if (originalFilename != null) {
|
if (originalFilename != null) {
|
||||||
String originalFilenameLower = originalFilename.toLowerCase();
|
String originalFilenameLower = originalFilename.toLowerCase();
|
||||||
|
|||||||
@@ -9,10 +9,11 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
|||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import io.github.pixee.security.Filenames;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.GeneralFile;
|
import stirling.software.SPDF.model.api.converters.HTMLToPdfRequest;
|
||||||
import stirling.software.SPDF.utils.FileToPdf;
|
import stirling.software.SPDF.utils.FileToPdf;
|
||||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
|
||||||
@@ -22,15 +23,16 @@ import stirling.software.SPDF.utils.WebResponseUtils;
|
|||||||
public class ConvertHtmlToPDF {
|
public class ConvertHtmlToPDF {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
@Qualifier("htmlFormatsInstalled")
|
@Qualifier("bookAndHtmlFormatsInstalled")
|
||||||
private boolean htmlFormatsInstalled;
|
private boolean bookAndHtmlFormatsInstalled;
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/html/pdf")
|
@PostMapping(consumes = "multipart/form-data", value = "/html/pdf")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Convert an HTML or ZIP (containing HTML and CSS) to PDF",
|
summary = "Convert an HTML or ZIP (containing HTML and CSS) to PDF",
|
||||||
description =
|
description =
|
||||||
"This endpoint takes an HTML or ZIP file input and converts it to a PDF format.")
|
"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 {
|
public ResponseEntity<byte[]> HtmlToPdf(@ModelAttribute HTMLToPdfRequest request)
|
||||||
|
throws Exception {
|
||||||
MultipartFile fileInput = request.getFileInput();
|
MultipartFile fileInput = request.getFileInput();
|
||||||
|
|
||||||
if (fileInput == null) {
|
if (fileInput == null) {
|
||||||
@@ -38,14 +40,17 @@ public class ConvertHtmlToPDF {
|
|||||||
"Please provide an HTML or ZIP file for conversion.");
|
"Please provide an HTML or ZIP file for conversion.");
|
||||||
}
|
}
|
||||||
|
|
||||||
String originalFilename = fileInput.getOriginalFilename();
|
String originalFilename = Filenames.toSimpleFileName(fileInput.getOriginalFilename());
|
||||||
if (originalFilename == null
|
if (originalFilename == null
|
||||||
|| (!originalFilename.endsWith(".html") && !originalFilename.endsWith(".zip"))) {
|
|| (!originalFilename.endsWith(".html") && !originalFilename.endsWith(".zip"))) {
|
||||||
throw new IllegalArgumentException("File must be either .html or .zip format.");
|
throw new IllegalArgumentException("File must be either .html or .zip format.");
|
||||||
}
|
}
|
||||||
byte[] pdfBytes =
|
byte[] pdfBytes =
|
||||||
FileToPdf.convertHtmlToPdf(
|
FileToPdf.convertHtmlToPdf(
|
||||||
fileInput.getBytes(), originalFilename, htmlFormatsInstalled);
|
request,
|
||||||
|
fileInput.getBytes(),
|
||||||
|
originalFilename,
|
||||||
|
bookAndHtmlFormatsInstalled);
|
||||||
|
|
||||||
String outputFilename =
|
String outputFilename =
|
||||||
originalFilename.replaceFirst("[.][^.]+$", "")
|
originalFilename.replaceFirst("[.][^.]+$", "")
|
||||||
|
|||||||
@@ -6,10 +6,6 @@ import java.net.URLConnection;
|
|||||||
import org.apache.pdfbox.rendering.ImageType;
|
import org.apache.pdfbox.rendering.ImageType;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.core.io.ByteArrayResource;
|
|
||||||
import org.springframework.core.io.Resource;
|
|
||||||
import org.springframework.http.HttpHeaders;
|
|
||||||
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.ModelAttribute;
|
||||||
@@ -18,6 +14,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
|||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import io.github.pixee.security.Filenames;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
@@ -38,8 +35,8 @@ public class ConvertImgPDFController {
|
|||||||
summary = "Convert PDF to image(s)",
|
summary = "Convert PDF to image(s)",
|
||||||
description =
|
description =
|
||||||
"This endpoint converts a PDF file to image(s) with the specified image format, color type, and DPI. Users can choose to get a single image or multiple images. Input:PDF Output:Image Type:SI-Conditional")
|
"This endpoint converts a PDF file to image(s) with the specified image format, color type, and DPI. Users can choose to get a single image or multiple images. Input:PDF Output:Image Type:SI-Conditional")
|
||||||
public ResponseEntity<Resource> convertToImage(@ModelAttribute ConvertToImageRequest request)
|
public ResponseEntity<byte[]> convertToImage(@ModelAttribute ConvertToImageRequest request)
|
||||||
throws IOException {
|
throws NumberFormatException, Exception {
|
||||||
MultipartFile file = request.getFileInput();
|
MultipartFile file = request.getFileInput();
|
||||||
String imageFormat = request.getImageFormat();
|
String imageFormat = request.getImageFormat();
|
||||||
String singleOrMultiple = request.getSingleOrMultiple();
|
String singleOrMultiple = request.getSingleOrMultiple();
|
||||||
@@ -54,41 +51,33 @@ public class ConvertImgPDFController {
|
|||||||
colorTypeResult = ImageType.BINARY;
|
colorTypeResult = ImageType.BINARY;
|
||||||
}
|
}
|
||||||
// returns bytes for image
|
// returns bytes for image
|
||||||
boolean singleImage = singleOrMultiple.equals("single");
|
boolean singleImage = "single".equals(singleOrMultiple);
|
||||||
byte[] result = null;
|
byte[] result = null;
|
||||||
String filename = file.getOriginalFilename().replaceFirst("[.][^.]+$", "");
|
String filename =
|
||||||
try {
|
Filenames.toSimpleFileName(file.getOriginalFilename())
|
||||||
result =
|
.replaceFirst("[.][^.]+$", "");
|
||||||
PdfUtils.convertFromPdf(
|
|
||||||
pdfBytes,
|
result =
|
||||||
imageFormat.toUpperCase(),
|
PdfUtils.convertFromPdf(
|
||||||
colorTypeResult,
|
pdfBytes,
|
||||||
singleImage,
|
imageFormat.toUpperCase(),
|
||||||
Integer.valueOf(dpi),
|
colorTypeResult,
|
||||||
filename);
|
singleImage,
|
||||||
} catch (IOException e) {
|
Integer.valueOf(dpi),
|
||||||
// TODO Auto-generated catch block
|
filename);
|
||||||
e.printStackTrace();
|
|
||||||
} catch (Exception e) {
|
|
||||||
// TODO Auto-generated catch block
|
if(result == null || result.length == 0) {
|
||||||
e.printStackTrace();
|
logger.error("resultant bytes for {} is null, error converting ", filename);
|
||||||
}
|
}
|
||||||
if (singleImage) {
|
if (singleImage) {
|
||||||
HttpHeaders headers = new HttpHeaders();
|
String docName = filename + "." + imageFormat;
|
||||||
headers.setContentType(MediaType.parseMediaType(getMediaType(imageFormat)));
|
MediaType mediaType = MediaType.parseMediaType(getMediaType(imageFormat));
|
||||||
ResponseEntity<Resource> response =
|
return WebResponseUtils.bytesToWebResponse(result, docName, mediaType);
|
||||||
new ResponseEntity<>(new ByteArrayResource(result), headers, HttpStatus.OK);
|
|
||||||
return response;
|
|
||||||
} else {
|
} else {
|
||||||
ByteArrayResource resource = new ByteArrayResource(result);
|
String zipFilename = filename + "_convertedToImages.zip";
|
||||||
// return the Resource in the response
|
return WebResponseUtils.bytesToWebResponse(
|
||||||
return ResponseEntity.ok()
|
result, zipFilename, MediaType.APPLICATION_OCTET_STREAM);
|
||||||
.header(
|
|
||||||
HttpHeaders.CONTENT_DISPOSITION,
|
|
||||||
"attachment; filename=" + filename + "_convertedToImages.zip")
|
|
||||||
.contentType(MediaType.APPLICATION_OCTET_STREAM)
|
|
||||||
.contentLength(resource.contentLength())
|
|
||||||
.body(resource);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,7 +85,7 @@ public class ConvertImgPDFController {
|
|||||||
@Operation(
|
@Operation(
|
||||||
summary = "Convert images to a PDF file",
|
summary = "Convert images to a PDF file",
|
||||||
description =
|
description =
|
||||||
"This endpoint converts one or more images to a PDF file. Users can specify whether to stretch the images to fit the PDF page, and whether to automatically rotate the images. Input:Image Output:PDF Type:SISO?")
|
"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:MISO")
|
||||||
public ResponseEntity<byte[]> convertToPdf(@ModelAttribute ConvertToPdfRequest request)
|
public ResponseEntity<byte[]> convertToPdf(@ModelAttribute ConvertToPdfRequest request)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
MultipartFile[] file = request.getFileInput();
|
MultipartFile[] file = request.getFileInput();
|
||||||
@@ -113,6 +102,6 @@ public class ConvertImgPDFController {
|
|||||||
|
|
||||||
private String getMediaType(String imageFormat) {
|
private String getMediaType(String imageFormat) {
|
||||||
String mimeType = URLConnection.guessContentTypeFromName("." + imageFormat);
|
String mimeType = URLConnection.guessContentTypeFromName("." + imageFormat);
|
||||||
return mimeType.equals("null") ? "application/octet-stream" : mimeType;
|
return "null".equals(mimeType) ? "application/octet-stream" : mimeType;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
|||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import io.github.pixee.security.Filenames;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
@@ -32,14 +33,14 @@ import stirling.software.SPDF.utils.WebResponseUtils;
|
|||||||
public class ConvertMarkdownToPdf {
|
public class ConvertMarkdownToPdf {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
@Qualifier("htmlFormatsInstalled")
|
@Qualifier("bookAndHtmlFormatsInstalled")
|
||||||
private boolean htmlFormatsInstalled;
|
private boolean bookAndHtmlFormatsInstalled;
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/markdown/pdf")
|
@PostMapping(consumes = "multipart/form-data", value = "/markdown/pdf")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Convert a Markdown file to PDF",
|
summary = "Convert a Markdown file to PDF",
|
||||||
description =
|
description =
|
||||||
"This endpoint takes a Markdown file input, converts it to HTML, and then to PDF format.")
|
"This endpoint takes a Markdown file input, converts it to HTML, and then to PDF format. Input:MARKDOWN Output:PDF Type:SISO")
|
||||||
public ResponseEntity<byte[]> markdownToPdf(@ModelAttribute GeneralFile request)
|
public ResponseEntity<byte[]> markdownToPdf(@ModelAttribute GeneralFile request)
|
||||||
throws Exception {
|
throws Exception {
|
||||||
MultipartFile fileInput = request.getFileInput();
|
MultipartFile fileInput = request.getFileInput();
|
||||||
@@ -48,7 +49,7 @@ public class ConvertMarkdownToPdf {
|
|||||||
throw new IllegalArgumentException("Please provide a Markdown file for conversion.");
|
throw new IllegalArgumentException("Please provide a Markdown file for conversion.");
|
||||||
}
|
}
|
||||||
|
|
||||||
String originalFilename = fileInput.getOriginalFilename();
|
String originalFilename = Filenames.toSimpleFileName(fileInput.getOriginalFilename());
|
||||||
if (originalFilename == null || !originalFilename.endsWith(".md")) {
|
if (originalFilename == null || !originalFilename.endsWith(".md")) {
|
||||||
throw new IllegalArgumentException("File must be in .md format.");
|
throw new IllegalArgumentException("File must be in .md format.");
|
||||||
}
|
}
|
||||||
@@ -68,7 +69,10 @@ public class ConvertMarkdownToPdf {
|
|||||||
|
|
||||||
byte[] pdfBytes =
|
byte[] pdfBytes =
|
||||||
FileToPdf.convertHtmlToPdf(
|
FileToPdf.convertHtmlToPdf(
|
||||||
htmlContent.getBytes(), "converted.html", htmlFormatsInstalled);
|
null,
|
||||||
|
htmlContent.getBytes(),
|
||||||
|
"converted.html",
|
||||||
|
bookAndHtmlFormatsInstalled);
|
||||||
|
|
||||||
String outputFilename =
|
String outputFilename =
|
||||||
originalFilename.replaceFirst("[.][^.]+$", "")
|
originalFilename.replaceFirst("[.][^.]+$", "")
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
|||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import io.github.pixee.security.Filenames;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
@@ -31,7 +32,7 @@ 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 = Filenames.toSimpleFileName(inputFile.getOriginalFilename());
|
||||||
if (originalFilename == null
|
if (originalFilename == null
|
||||||
|| !isValidFileExtension(FilenameUtils.getExtension(originalFilename))) {
|
|| !isValidFileExtension(FilenameUtils.getExtension(originalFilename))) {
|
||||||
throw new IllegalArgumentException("Invalid file extension");
|
throw new IllegalArgumentException("Invalid file extension");
|
||||||
@@ -89,7 +90,8 @@ public class ConvertOfficeController {
|
|||||||
byte[] pdfByteArray = convertToPdf(inputFile);
|
byte[] pdfByteArray = convertToPdf(inputFile);
|
||||||
return WebResponseUtils.bytesToWebResponse(
|
return WebResponseUtils.bytesToWebResponse(
|
||||||
pdfByteArray,
|
pdfByteArray,
|
||||||
inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "")
|
Filenames.toSimpleFileName(inputFile.getOriginalFilename())
|
||||||
|
.replaceFirst("[.][^.]+$", "")
|
||||||
+ "_convertedToPDF.pdf");
|
+ "_convertedToPDF.pdf");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
|||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import io.github.pixee.security.Filenames;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
@@ -29,22 +30,22 @@ import stirling.software.SPDF.utils.WebResponseUtils;
|
|||||||
public class ConvertPDFToBookController {
|
public class ConvertPDFToBookController {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
@Qualifier("bookFormatsInstalled")
|
@Qualifier("bookAndHtmlFormatsInstalled")
|
||||||
private boolean bookFormatsInstalled;
|
private boolean bookAndHtmlFormatsInstalled;
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/pdf/book")
|
@PostMapping(consumes = "multipart/form-data", value = "/pdf/book")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary =
|
summary =
|
||||||
"Convert a PDF to a Book/comic (*.epub | *.mobi | *.azw3 | *.fb2 | *.txt | *.docx .. (others to include by chatgpt) to PDF",
|
"Convert a PDF to a Book/comic (*.epub | *.mobi | *.azw3 | *.fb2 | *.txt | *.docx .. (others to include by chatgpt) to PDF",
|
||||||
description =
|
description =
|
||||||
"(Requires bookFormatsInstalled flag and Calibre installed) This endpoint Convert a PDF to a Book/comic (*.epub | *.mobi | *.azw3 | *.fb2 | *.txt | *.docx .. (others to include by chatgpt) to PDF")
|
"(Requires bookAndHtmlFormatsInstalled flag and Calibre installed) This endpoint Convert a PDF to a Book/comic (*.epub | *.mobi | *.azw3 | *.fb2 | *.txt | *.docx .. (others to include by chatgpt) to PDF")
|
||||||
public ResponseEntity<byte[]> HtmlToPdf(@ModelAttribute PdfToBookRequest request)
|
public ResponseEntity<byte[]> HtmlToPdf(@ModelAttribute PdfToBookRequest request)
|
||||||
throws Exception {
|
throws Exception {
|
||||||
MultipartFile fileInput = request.getFileInput();
|
MultipartFile fileInput = request.getFileInput();
|
||||||
|
|
||||||
if (!bookFormatsInstalled) {
|
if (!bookAndHtmlFormatsInstalled) {
|
||||||
throw new IllegalArgumentException(
|
throw new IllegalArgumentException(
|
||||||
"bookFormatsInstalled flag is False, this functionality is not avaiable");
|
"bookAndHtmlFormatsInstalled flag is False, this functionality is not available");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fileInput == null) {
|
if (fileInput == null) {
|
||||||
@@ -92,7 +93,8 @@ public class ConvertPDFToBookController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String outputFilename =
|
String outputFilename =
|
||||||
fileInput.getOriginalFilename().replaceFirst("[.][^.]+$", "")
|
Filenames.toSimpleFileName(fileInput.getOriginalFilename())
|
||||||
|
.replaceFirst("[.][^.]+$", "")
|
||||||
+ "."
|
+ "."
|
||||||
+ outputFormat; // Remove file extension and append .pdf
|
+ outputFormat; // Remove file extension and append .pdf
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
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.PDFFile;
|
||||||
|
import stirling.software.SPDF.utils.PDFToFile;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@Tag(name = "Convert", description = "Convert APIs")
|
||||||
|
@RequestMapping("/api/v1/convert")
|
||||||
|
public class ConvertPDFToHtml {
|
||||||
|
|
||||||
|
@PostMapping(consumes = "multipart/form-data", value = "/pdf/html")
|
||||||
|
@Operation(
|
||||||
|
summary = "Convert PDF to HTML",
|
||||||
|
description =
|
||||||
|
"This endpoint converts a PDF file to HTML format. Input:PDF Output:HTML Type:SISO")
|
||||||
|
public ResponseEntity<byte[]> processPdfToHTML(@ModelAttribute PDFFile request)
|
||||||
|
throws Exception {
|
||||||
|
MultipartFile inputFile = request.getFileInput();
|
||||||
|
PDFToFile pdfToFile = new PDFToFile();
|
||||||
|
return pdfToFile.processPdfToHtml(inputFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,10 @@ package stirling.software.SPDF.controller.api.converters;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import org.apache.pdfbox.Loader;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
|
import org.apache.pdfbox.text.PDFTextStripper;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
@@ -9,6 +13,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
|||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import io.github.pixee.security.Filenames;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
@@ -17,24 +22,13 @@ import stirling.software.SPDF.model.api.converters.PdfToPresentationRequest;
|
|||||||
import stirling.software.SPDF.model.api.converters.PdfToTextOrRTFRequest;
|
import stirling.software.SPDF.model.api.converters.PdfToTextOrRTFRequest;
|
||||||
import stirling.software.SPDF.model.api.converters.PdfToWordRequest;
|
import stirling.software.SPDF.model.api.converters.PdfToWordRequest;
|
||||||
import stirling.software.SPDF.utils.PDFToFile;
|
import stirling.software.SPDF.utils.PDFToFile;
|
||||||
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/convert")
|
@RequestMapping("/api/v1/convert")
|
||||||
@Tag(name = "Convert", description = "Convert APIs")
|
@Tag(name = "Convert", description = "Convert APIs")
|
||||||
public class ConvertPDFToOffice {
|
public class ConvertPDFToOffice {
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/pdf/html")
|
|
||||||
@Operation(
|
|
||||||
summary = "Convert PDF to HTML",
|
|
||||||
description =
|
|
||||||
"This endpoint converts a PDF file to HTML format. Input:PDF Output:HTML Type:SISO")
|
|
||||||
public ResponseEntity<byte[]> processPdfToHTML(@ModelAttribute PDFFile request)
|
|
||||||
throws Exception {
|
|
||||||
MultipartFile inputFile = request.getFileInput();
|
|
||||||
PDFToFile pdfToFile = new PDFToFile();
|
|
||||||
return pdfToFile.processPdfToOfficeFormat(inputFile, "html", "writer_pdf_import");
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/pdf/presentation")
|
@PostMapping(consumes = "multipart/form-data", value = "/pdf/presentation")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Convert PDF to Presentation format",
|
summary = "Convert PDF to Presentation format",
|
||||||
@@ -59,9 +53,21 @@ public class ConvertPDFToOffice {
|
|||||||
throws IOException, InterruptedException {
|
throws IOException, InterruptedException {
|
||||||
MultipartFile inputFile = request.getFileInput();
|
MultipartFile inputFile = request.getFileInput();
|
||||||
String outputFormat = request.getOutputFormat();
|
String outputFormat = request.getOutputFormat();
|
||||||
|
if ("txt".equals(request.getOutputFormat())) {
|
||||||
PDFToFile pdfToFile = new PDFToFile();
|
try (PDDocument document = Loader.loadPDF(inputFile.getBytes())) {
|
||||||
return pdfToFile.processPdfToOfficeFormat(inputFile, outputFormat, "writer_pdf_import");
|
PDFTextStripper stripper = new PDFTextStripper();
|
||||||
|
String text = stripper.getText(document);
|
||||||
|
return WebResponseUtils.bytesToWebResponse(
|
||||||
|
text.getBytes(),
|
||||||
|
Filenames.toSimpleFileName(inputFile.getOriginalFilename())
|
||||||
|
.replaceFirst("[.][^.]+$", "")
|
||||||
|
+ ".txt",
|
||||||
|
MediaType.TEXT_PLAIN);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
PDFToFile pdfToFile = new PDFToFile();
|
||||||
|
return pdfToFile.processPdfToOfficeFormat(inputFile, outputFormat, "writer_pdf_import");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/pdf/word")
|
@PostMapping(consumes = "multipart/form-data", value = "/pdf/word")
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user