Compare commits
293 Commits
bug/rememb
...
v0.36.5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8dca4a588d | ||
|
|
63386baa0d | ||
|
|
2a93910da3 | ||
|
|
fca6dc1fd8 | ||
|
|
2d82c5fa77 | ||
|
|
167c792bf0 | ||
|
|
95a9e10dc8 | ||
|
|
4d6368048c | ||
|
|
4e715a82e0 | ||
|
|
164381e940 | ||
|
|
0436f45de5 | ||
|
|
6ce761aff2 | ||
|
|
8a5d9f9a95 | ||
|
|
c0ef624a1d | ||
|
|
b9ae90274f | ||
|
|
764b8f4d22 | ||
|
|
a531f53893 | ||
|
|
74d6d96f4e | ||
|
|
1862ab1671 | ||
|
|
bf95ca43dc | ||
|
|
678ef85da1 | ||
|
|
9a6afdd921 | ||
|
|
a72615cc86 | ||
|
|
b0daac2566 | ||
|
|
2335ecf017 | ||
|
|
a79318bf0c | ||
|
|
9eed761346 | ||
|
|
12d86049f6 | ||
|
|
e9f80d03ea | ||
|
|
42c238d87d | ||
|
|
051cd2d0d5 | ||
|
|
eb1301464d | ||
|
|
720705e422 | ||
|
|
db3a8a8483 | ||
|
|
2bdda9acba | ||
|
|
8053035158 | ||
|
|
bd20d3ad93 | ||
|
|
c6700b34b0 | ||
|
|
25b66a0775 | ||
|
|
ac9f4a4c7e | ||
|
|
3a0d89bd7c | ||
|
|
5d4769df56 | ||
|
|
80b9d2691a | ||
|
|
99c598096e | ||
|
|
b3dfdba9cb | ||
|
|
c2e96c5f1d | ||
|
|
cffdbc5e7b | ||
|
|
e8d7b4aded | ||
|
|
d672ad22bc | ||
|
|
2c0a9ae082 | ||
|
|
bbedf22678 | ||
|
|
e9431ba6b5 | ||
|
|
d74c521bc0 | ||
|
|
fa57ee4ef9 | ||
|
|
be3cbcc7cd | ||
|
|
317d985166 | ||
|
|
64f8348215 | ||
|
|
7f045dbcc7 | ||
|
|
343e38a5fd | ||
|
|
af100d4190 | ||
|
|
4998ad064a | ||
|
|
64ee26facf | ||
|
|
e273b9a3ad | ||
|
|
42541a7174 | ||
|
|
ca3002f925 | ||
|
|
6c172af3c1 | ||
|
|
60ced19f64 | ||
|
|
88079ddf7f | ||
|
|
9b6dcdcd06 | ||
|
|
63eb94c0a6 | ||
|
|
0a6b6453b2 | ||
|
|
2931e348a9 | ||
|
|
c71ca21532 | ||
|
|
0e4c3d5dbc | ||
|
|
92cabf125e | ||
|
|
818bed3154 | ||
|
|
faf3454a02 | ||
|
|
1f1c414138 | ||
|
|
f1c5384a37 | ||
|
|
5607f7079e | ||
|
|
3b8723975d | ||
|
|
41a3d28c90 | ||
|
|
f7afe73cb4 | ||
|
|
73e5246191 | ||
|
|
1f39481efe | ||
|
|
5ac2260d78 | ||
|
|
bae83a281c | ||
|
|
dd2aae60ad | ||
|
|
30ee33002d | ||
|
|
24717dde19 | ||
|
|
509a305985 | ||
|
|
13572a7f18 | ||
|
|
ebd0ddc6ad | ||
|
|
43c4ec1089 | ||
|
|
1ccdc1697b | ||
|
|
2297c5dc95 | ||
|
|
859c9942e6 | ||
|
|
c723696bd0 | ||
|
|
fe198458ca | ||
|
|
378aca4460 | ||
|
|
9870e6ad7c | ||
|
|
40b5904726 | ||
|
|
de3f59cf44 | ||
|
|
4ae7c83357 | ||
|
|
f127271709 | ||
|
|
f899088c75 | ||
|
|
d55e007f93 | ||
|
|
ccdcb05e65 | ||
|
|
e6c2ad8c9c | ||
|
|
32aa623c8b | ||
|
|
23888c5d2c | ||
|
|
2f23eb69c6 | ||
|
|
446cedf26e | ||
|
|
f950e25dad | ||
|
|
65a86a6e97 | ||
|
|
7f4134f52d | ||
|
|
55969697b8 | ||
|
|
86662d9cf6 | ||
|
|
2437460c73 | ||
|
|
a7960d992c | ||
|
|
c98cd8117f | ||
|
|
50c5efac87 | ||
|
|
0952245b23 | ||
|
|
83ddfdf152 | ||
|
|
08777c78b1 | ||
|
|
c6980e9693 | ||
|
|
fc514ee65b | ||
|
|
44be2b99d2 | ||
|
|
549824c91f | ||
|
|
1c0d1efda6 | ||
|
|
9d8d90bf2f | ||
|
|
a7e7d57b23 | ||
|
|
e014bb023b | ||
|
|
1c5dfc46a0 | ||
|
|
11273b1589 | ||
|
|
dd5af46906 | ||
|
|
c20d37518d | ||
|
|
eb20f51958 | ||
|
|
97d28ac6d2 | ||
|
|
026fe8150d | ||
|
|
c3f88f716c | ||
|
|
67f983f00d | ||
|
|
9167f12296 | ||
|
|
93e190fdeb | ||
|
|
82bebf5c62 | ||
|
|
bb3f076e6d | ||
|
|
64dfa4b841 | ||
|
|
0f6f3f305a | ||
|
|
58c7d7b9a8 | ||
|
|
ef8231de3a | ||
|
|
c1c3eba398 | ||
|
|
52693541d9 | ||
|
|
1639e0fc4c | ||
|
|
0652299bec | ||
|
|
1d6511b043 | ||
|
|
a400fe6015 | ||
|
|
b47df3d252 | ||
|
|
cb6e1cd94e | ||
|
|
6ee6254f5a | ||
|
|
f2c9549ba1 | ||
|
|
58278c07ff | ||
|
|
4d017610b8 | ||
|
|
dcafc0d487 | ||
|
|
2ec8c97737 | ||
|
|
397a07afe8 | ||
|
|
b072c39fd9 | ||
|
|
1bc6b4149c | ||
|
|
5a5a8bb7ba | ||
|
|
b6eca59f23 | ||
|
|
7108424a92 | ||
|
|
400965ffc8 | ||
|
|
1895a04394 | ||
|
|
f8f137a30a | ||
|
|
f6a2d4784b | ||
|
|
526dc9f911 | ||
|
|
cce9f74eb9 | ||
|
|
0e3865618d | ||
|
|
d888ed1ae0 | ||
|
|
99d1b46d97 | ||
|
|
32e46eeb73 | ||
|
|
b7da84d257 | ||
|
|
1c1ead5d62 | ||
|
|
6ff53aa5b3 | ||
|
|
8d60b08cd9 | ||
|
|
64cf5167c0 | ||
|
|
de4637e8d4 | ||
|
|
3c0a8071dc | ||
|
|
04ccdf6f76 | ||
|
|
db02fba31f | ||
|
|
5b6f649e4e | ||
|
|
de23bb702c | ||
|
|
25e564154e | ||
|
|
3633a979d3 | ||
|
|
99d481d69f | ||
|
|
a5ba6c403a | ||
|
|
b2e6d89d16 | ||
|
|
b59d2d15b4 | ||
|
|
61e750646c | ||
|
|
de9c21b3de | ||
|
|
b32d6cb858 | ||
|
|
d832a90de0 | ||
|
|
212e521238 | ||
|
|
0915e72a3d | ||
|
|
ee5013651f | ||
|
|
4aa44e6fc0 | ||
|
|
41c743a9f8 | ||
|
|
833b3c45c6 | ||
|
|
654bc94d44 | ||
|
|
86fa404c90 | ||
|
|
1db1370420 | ||
|
|
ee4b7e02ab | ||
|
|
e6c5634165 | ||
|
|
5188eb3b04 | ||
|
|
3fa6bcb2ee | ||
|
|
0b359ad4a8 | ||
|
|
23ee77f6ab | ||
|
|
bfc1ed2b39 | ||
|
|
da46d942ba | ||
|
|
5936e856f0 | ||
|
|
fd906d36dd | ||
|
|
8f4709d82e | ||
|
|
8445f2719b | ||
|
|
eaa64e1471 | ||
|
|
4abb0cb85e | ||
|
|
afad06bed4 | ||
|
|
faa8a9752c | ||
|
|
989538e340 | ||
|
|
5b8bdc3352 | ||
|
|
f559eaa4e8 | ||
|
|
f306e00fba | ||
|
|
e09d6f9998 | ||
|
|
3a27aa16d5 | ||
|
|
9a96109ea2 | ||
|
|
ad1cce378f | ||
|
|
9abb105835 | ||
|
|
204bae3bc1 | ||
|
|
547f23fe78 | ||
|
|
543ad083a2 | ||
|
|
61bccd1d8b | ||
|
|
83be709299 | ||
|
|
0e602153f3 | ||
|
|
597619740a | ||
|
|
41a39a0a94 | ||
|
|
b14fba064d | ||
|
|
bd29dd1ac3 | ||
|
|
6d3f14375e | ||
|
|
888aec5701 | ||
|
|
92e7e85e77 | ||
|
|
3bf467e4ff | ||
|
|
dc1887db4d | ||
|
|
bab2052a60 | ||
|
|
7773df7443 | ||
|
|
32d575b4e9 | ||
|
|
4ebeedc028 | ||
|
|
b4b005bc2e | ||
|
|
b92bcfe915 | ||
|
|
aeca2b23d9 | ||
|
|
68349c4426 | ||
|
|
0f6d5e5a41 | ||
|
|
df1c5476d9 | ||
|
|
d0d6a70250 | ||
|
|
a6ae3734ca | ||
|
|
c239d95131 | ||
|
|
d591874da6 | ||
|
|
6c623d8d84 | ||
|
|
e059caa14e | ||
|
|
8eab35761d | ||
|
|
c43af24ffe | ||
|
|
e1b3cc736c | ||
|
|
0fb9e18636 | ||
|
|
5e1aac0b84 | ||
|
|
60bf649260 | ||
|
|
a58696a38e | ||
|
|
44abc67678 | ||
|
|
d1e690ff8d | ||
|
|
5dc8fa08ee | ||
|
|
db028dfe27 | ||
|
|
c24c504350 | ||
|
|
5dcfe64d1c | ||
|
|
d843696703 | ||
|
|
67de8a9460 | ||
|
|
b26aa3417e | ||
|
|
8dfb5940ca | ||
|
|
0ce479e1e3 | ||
|
|
cca3b6b525 | ||
|
|
03529567ba | ||
|
|
781a52c759 | ||
|
|
be2c103065 | ||
|
|
80fd2eff5f | ||
|
|
65abfd9c7a | ||
|
|
f4082e3f96 | ||
|
|
c93a48b40d | ||
|
|
75e10efcbd |
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,5 +1,5 @@
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: 💬 Discord Server
|
||||
url: https://discord.gg/Cn8pWhQRxZ
|
||||
url: https://discord.gg/HYmhKj45pU
|
||||
about: You can join our Discord server for real time discussion and support
|
||||
|
||||
117
.github/scripts/check_language_properties.py
vendored
117
.github/scripts/check_language_properties.py
vendored
@@ -9,8 +9,9 @@ The script also provides functionality to update the translation files to match
|
||||
adjusting the format.
|
||||
|
||||
Usage:
|
||||
python script_name.py --reference-file <path_to_reference_file> --branch <branch_name> [--files <list_of_changed_files>]
|
||||
python check_language_properties.py --reference-file <path_to_reference_file> --branch <branch_name> [--actor <actor_name>] [--files <list_of_changed_files>]
|
||||
"""
|
||||
|
||||
import copy
|
||||
import glob
|
||||
import os
|
||||
@@ -18,6 +19,10 @@ import argparse
|
||||
import re
|
||||
|
||||
|
||||
# Maximum size for properties files (e.g., 200 KB)
|
||||
MAX_FILE_SIZE = 200 * 1024
|
||||
|
||||
|
||||
def parse_properties_file(file_path):
|
||||
"""Parses a .properties file and returns a list of objects (including comments, empty lines, and line numbers)."""
|
||||
properties_list = []
|
||||
@@ -95,7 +100,7 @@ def write_json_file(file_path, updated_properties):
|
||||
def update_missing_keys(reference_file, file_list, branch=""):
|
||||
reference_properties = parse_properties_file(reference_file)
|
||||
for file_path in file_list:
|
||||
basename_current_file = os.path.basename(branch + file_path)
|
||||
basename_current_file = os.path.basename(os.path.join(branch, file_path))
|
||||
if (
|
||||
basename_current_file == os.path.basename(reference_file)
|
||||
or not file_path.endswith(".properties")
|
||||
@@ -103,7 +108,7 @@ def update_missing_keys(reference_file, file_list, branch=""):
|
||||
):
|
||||
continue
|
||||
|
||||
current_properties = parse_properties_file(branch + file_path)
|
||||
current_properties = parse_properties_file(os.path.join(branch, file_path))
|
||||
updated_properties = []
|
||||
for ref_entry in reference_properties:
|
||||
ref_entry_copy = copy.deepcopy(ref_entry)
|
||||
@@ -114,60 +119,79 @@ def update_missing_keys(reference_file, file_list, branch=""):
|
||||
if ref_entry_copy["key"] == current_entry["key"]:
|
||||
ref_entry_copy["value"] = current_entry["value"]
|
||||
updated_properties.append(ref_entry_copy)
|
||||
write_json_file(branch + file_path, updated_properties)
|
||||
write_json_file(os.path.join(branch, file_path), updated_properties)
|
||||
|
||||
|
||||
def check_for_missing_keys(reference_file, file_list, branch):
|
||||
update_missing_keys(reference_file, file_list, branch + "/")
|
||||
update_missing_keys(reference_file, file_list, branch)
|
||||
|
||||
|
||||
def read_properties(file_path):
|
||||
with open(file_path, "r", encoding="utf-8") as file:
|
||||
return file.read().splitlines()
|
||||
if os.path.isfile(file_path) and os.path.exists(file_path):
|
||||
with open(file_path, "r", encoding="utf-8") as file:
|
||||
return file.read().splitlines()
|
||||
return [""]
|
||||
|
||||
|
||||
def check_for_differences(reference_file, file_list, branch):
|
||||
def check_for_differences(reference_file, file_list, branch, actor):
|
||||
reference_branch = reference_file.split("/")[0]
|
||||
basename_reference_file = os.path.basename(reference_file)
|
||||
|
||||
report = []
|
||||
report.append(
|
||||
f"### 📋 Checking with the file `{basename_reference_file}` from the `{reference_branch}` - Checking the `{branch}`"
|
||||
)
|
||||
report.append(f"#### 🔄 Reference Branch: `{reference_branch}`")
|
||||
reference_lines = read_properties(reference_file)
|
||||
has_differences = False
|
||||
|
||||
only_reference_file = True
|
||||
|
||||
for file_path in file_list:
|
||||
basename_current_file = os.path.basename(branch + "/" + file_path)
|
||||
file_arr = file_list
|
||||
|
||||
if len(file_list) == 1:
|
||||
file_arr = file_list[0].split()
|
||||
base_dir = os.path.abspath(os.path.join(os.getcwd(), "src", "main", "resources"))
|
||||
|
||||
for file_path in file_arr:
|
||||
absolute_path = os.path.abspath(file_path)
|
||||
# Verify that file is within the expected directory
|
||||
if not absolute_path.startswith(base_dir):
|
||||
raise ValueError(f"Unsafe file found: {file_path}")
|
||||
# Verify file size before processing
|
||||
if os.path.getsize(os.path.join(branch, file_path)) > MAX_FILE_SIZE:
|
||||
raise ValueError(
|
||||
f"The file {file_path} is too large and could pose a security risk."
|
||||
)
|
||||
|
||||
basename_current_file = os.path.basename(os.path.join(branch, file_path))
|
||||
if (
|
||||
basename_current_file == basename_reference_file
|
||||
or not file_path.startswith(
|
||||
os.path.join("src", "main", "resources", "messages_")
|
||||
)
|
||||
or not file_path.endswith(".properties")
|
||||
or not basename_current_file.startswith("messages_")
|
||||
):
|
||||
continue
|
||||
only_reference_file = False
|
||||
report.append(f"#### 🗂️ **Checking File:** `{basename_current_file}`...")
|
||||
current_lines = read_properties(branch + "/" + file_path)
|
||||
report.append(f"#### 📃 **File Check:** `{basename_current_file}`")
|
||||
current_lines = read_properties(os.path.join(branch, file_path))
|
||||
reference_line_count = len(reference_lines)
|
||||
current_line_count = len(current_lines)
|
||||
|
||||
if reference_line_count != current_line_count:
|
||||
report.append("")
|
||||
report.append("- **Test 1 Status:** ❌ Failed")
|
||||
report.append("1. **Test Status:** ❌ **_Failed_**")
|
||||
report.append(" - **Issue:**")
|
||||
has_differences = True
|
||||
if reference_line_count > current_line_count:
|
||||
report.append(
|
||||
f" - **Issue:** Missing lines! Comments, empty lines, or translation strings are missing. Details: {reference_line_count} (reference) vs {current_line_count} (current)."
|
||||
f" - **_Mismatched line count_**: {reference_line_count} (reference) vs {current_line_count} (current). Comments, empty lines, or translation strings are missing."
|
||||
)
|
||||
elif reference_line_count < current_line_count:
|
||||
report.append(
|
||||
f" - **Issue:** Too many lines! Check your translation files! Details: {reference_line_count} (reference) vs {current_line_count} (current)."
|
||||
f" - **_Too many lines_**: {reference_line_count} (reference) vs {current_line_count} (current). Please verify if there is an additional line that needs to be removed."
|
||||
)
|
||||
# update_missing_keys(reference_file, [file_path], branch + "/")
|
||||
else:
|
||||
report.append("- **Test 1 Status:** ✅ Passed")
|
||||
report.append("1. **Test Status:** ✅ **_Passed_**")
|
||||
|
||||
# Check for missing or extra keys
|
||||
current_keys = []
|
||||
@@ -192,32 +216,42 @@ def check_for_differences(reference_file, file_list, branch):
|
||||
has_differences = True
|
||||
missing_keys_str = "`, `".join(missing_keys_list)
|
||||
extra_keys_str = "`, `".join(extra_keys_list)
|
||||
report.append("- **Test 2 Status:** ❌ Failed")
|
||||
report.append("2. **Test Status:** ❌ **_Failed_**")
|
||||
report.append(" - **Issue:**")
|
||||
if missing_keys_list:
|
||||
spaces_keys_list = []
|
||||
for key in missing_keys_list:
|
||||
if " " in key:
|
||||
spaces_keys_list.append(key)
|
||||
if spaces_keys_list:
|
||||
spaces_keys_str = "`, `".join(spaces_keys_list)
|
||||
report.append(
|
||||
f" - **_Keys containing unnecessary spaces_**: `{spaces_keys_str}`!"
|
||||
)
|
||||
report.append(
|
||||
f" - **Issue:** There are keys in ***{basename_current_file}*** `{missing_keys_str}` that are not present in ***{basename_reference_file}***!"
|
||||
f" - **_Extra keys in `{basename_current_file}`_**: `{missing_keys_str}` that are not present in **_`{basename_reference_file}`_**."
|
||||
)
|
||||
if extra_keys_list:
|
||||
report.append(
|
||||
f" - **Issue:** There are keys in ***{basename_reference_file}*** `{extra_keys_str}` that are not present in ***{basename_current_file}***!"
|
||||
f" - **_Missing keys in `{basename_reference_file}`_**: `{extra_keys_str}` that are not present in **_`{basename_current_file}`_**."
|
||||
)
|
||||
# update_missing_keys(reference_file, [file_path], branch + "/")
|
||||
else:
|
||||
report.append("- **Test 2 Status:** ✅ Passed")
|
||||
# if has_differences:
|
||||
# report.append("")
|
||||
# report.append(f"#### 🚧 ***{basename_current_file}*** will be corrected...")
|
||||
report.append("2. **Test Status:** ✅ **_Passed_**")
|
||||
report.append("")
|
||||
report.append("---")
|
||||
report.append("")
|
||||
# update_file_list = glob.glob(branch + "/src/**/messages_*.properties", recursive=True)
|
||||
# update_missing_keys(reference_file, update_file_list)
|
||||
# report.append("---")
|
||||
# report.append("")
|
||||
if has_differences:
|
||||
report.append("## ❌ Overall Check Status: **_Failed_**")
|
||||
report.append("")
|
||||
report.append(
|
||||
f"@{actor} please check your translation if it conforms to the standard. Follow the format of [messages_en_GB.properties](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/src/main/resources/messages_en_GB.properties)"
|
||||
)
|
||||
else:
|
||||
report.append("## ✅ Overall Check Status: **_Success_**")
|
||||
report.append("")
|
||||
report.append(
|
||||
f"Thanks @{actor} for your help in keeping the translations up to date."
|
||||
)
|
||||
|
||||
if not only_reference_file:
|
||||
print("\n".join(report))
|
||||
@@ -225,6 +259,11 @@ def check_for_differences(reference_file, file_list, branch):
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Find missing keys")
|
||||
parser.add_argument(
|
||||
"--actor",
|
||||
required=False,
|
||||
help="Actor from PR.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--reference-file",
|
||||
required=True,
|
||||
@@ -244,11 +283,21 @@ if __name__ == "__main__":
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
# Sanitize --actor input to avoid injection attacks
|
||||
if args.actor:
|
||||
args.actor = re.sub(r"[^a-zA-Z0-9_\\-]", "", args.actor)
|
||||
|
||||
# Sanitize --branch input to avoid injection attacks
|
||||
if args.branch:
|
||||
args.branch = re.sub(r"[^a-zA-Z0-9\\-]", "", args.branch)
|
||||
|
||||
file_list = args.files
|
||||
if file_list is None:
|
||||
file_list = glob.glob(
|
||||
os.getcwd() + "/src/**/messages_*.properties", recursive=True
|
||||
os.path.join(
|
||||
os.getcwd(), "src", "main", "resources", "messages_*.properties"
|
||||
)
|
||||
)
|
||||
update_missing_keys(args.reference_file, file_list)
|
||||
else:
|
||||
check_for_differences(args.reference_file, file_list, args.branch)
|
||||
check_for_differences(args.reference_file, file_list, args.branch, args.actor)
|
||||
|
||||
179
.github/workflows/PR-Demo-Comment.yml
vendored
Normal file
179
.github/workflows/PR-Demo-Comment.yml
vendored
Normal file
@@ -0,0 +1,179 @@
|
||||
name: PR Deployment via Comment
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
check-comment:
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
github.event.issue.pull_request &&
|
||||
(
|
||||
contains(github.event.comment.body, 'prdeploy') ||
|
||||
contains(github.event.comment.body, 'deploypr')
|
||||
)
|
||||
&&
|
||||
(
|
||||
github.event.comment.user.login == 'frooodle' ||
|
||||
github.event.comment.user.login == 'sf298' ||
|
||||
github.event.comment.user.login == 'Ludy87' ||
|
||||
github.event.comment.user.login == 'LaserKaspar' ||
|
||||
github.event.comment.user.login == 'sbplat' ||
|
||||
github.event.comment.user.login == 'reecebrowne'
|
||||
)
|
||||
outputs:
|
||||
pr_number: ${{ steps.get-pr.outputs.pr_number }}
|
||||
pr_repository: ${{ steps.get-pr-info.outputs.repository }}
|
||||
pr_ref: ${{ steps.get-pr-info.outputs.ref }}
|
||||
|
||||
steps:
|
||||
- name: Get PR data
|
||||
id: get-pr
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const prNumber = context.payload.issue.number;
|
||||
console.log(`PR Number: ${prNumber}`);
|
||||
core.setOutput('pr_number', prNumber);
|
||||
|
||||
- name: Get PR repository and ref
|
||||
id: get-pr-info
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const { owner, repo } = context.repo;
|
||||
const prNumber = context.payload.issue.number;
|
||||
|
||||
const { data: pr } = await github.rest.pulls.get({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: prNumber,
|
||||
});
|
||||
|
||||
// For forks, use the full repository name, for internal PRs use the current repo
|
||||
const repository = pr.head.repo.fork ? pr.head.repo.full_name : `${owner}/${repo}`;
|
||||
|
||||
console.log(`PR Repository: ${repository}`);
|
||||
console.log(`PR Branch: ${pr.head.ref}`);
|
||||
|
||||
core.setOutput('repository', repository);
|
||||
core.setOutput('ref', pr.head.ref);
|
||||
|
||||
deploy-pr:
|
||||
needs: check-comment
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout PR
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: ${{ needs.check-comment.outputs.pr_repository }}
|
||||
ref: ${{ needs.check-comment.outputs.pr_ref }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
|
||||
- name: Run Gradle Command
|
||||
run: ./gradlew clean build
|
||||
env:
|
||||
DOCKER_ENABLE_SECURITY: false
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Get version number
|
||||
id: versionNumber
|
||||
run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_API }}
|
||||
|
||||
- name: Build and push PR-specific image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
push: true
|
||||
tags: ${{ secrets.DOCKER_HUB_USERNAME }}/test:pr-${{ needs.check-comment.outputs.pr_number }}
|
||||
build-args: VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
|
||||
platforms: linux/amd64
|
||||
|
||||
- name: Set up SSH
|
||||
run: |
|
||||
mkdir -p ~/.ssh/
|
||||
echo "${{ secrets.VPS_SSH_KEY }}" > ../private.key
|
||||
sudo chmod 600 ../private.key
|
||||
|
||||
- name: Deploy to VPS
|
||||
run: |
|
||||
# First create the docker-compose content locally
|
||||
cat > docker-compose.yml << 'EOF'
|
||||
version: '3.3'
|
||||
services:
|
||||
stirling-pdf:
|
||||
container_name: stirling-pdf-pr-${{ needs.check-comment.outputs.pr_number }}
|
||||
image: ${{ secrets.DOCKER_HUB_USERNAME }}/test:pr-${{ needs.check-comment.outputs.pr_number }}
|
||||
ports:
|
||||
- "${{ needs.check-comment.outputs.pr_number }}:8080"
|
||||
volumes:
|
||||
- /stirling/PR-${{ needs.check-comment.outputs.pr_number }}/data:/usr/share/tessdata:rw
|
||||
- /stirling/PR-${{ needs.check-comment.outputs.pr_number }}/config:/configs:rw
|
||||
- /stirling/PR-${{ needs.check-comment.outputs.pr_number }}/logs:/logs:rw
|
||||
environment:
|
||||
DOCKER_ENABLE_SECURITY: "false"
|
||||
SECURITY_ENABLELOGIN: "false"
|
||||
SYSTEM_DEFAULTLOCALE: en-GB
|
||||
UI_APPNAME: "Stirling-PDF PR#${{ needs.check-comment.outputs.pr_number }}"
|
||||
UI_HOMEDESCRIPTION: "PR#${{ needs.check-comment.outputs.pr_number }} for Stirling-PDF Latest"
|
||||
UI_APPNAMENAVBAR: "PR#${{ needs.check-comment.outputs.pr_number }}"
|
||||
SYSTEM_MAXFILESIZE: "100"
|
||||
METRICS_ENABLED: "true"
|
||||
SYSTEM_GOOGLEVISIBILITY: "false"
|
||||
restart: on-failure:5
|
||||
EOF
|
||||
|
||||
# Then copy the file and execute commands
|
||||
scp -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null docker-compose.yml ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }}:/tmp/docker-compose.yml
|
||||
|
||||
ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }} << 'ENDSSH'
|
||||
# Create PR-specific directories
|
||||
mkdir -p /stirling/PR-${{ needs.check-comment.outputs.pr_number }}/{data,config,logs}
|
||||
|
||||
# Move docker-compose file to correct location
|
||||
mv /tmp/docker-compose.yml /stirling/PR-${{ needs.check-comment.outputs.pr_number }}/docker-compose.yml
|
||||
|
||||
# Start or restart the container
|
||||
cd /stirling/PR-${{ needs.check-comment.outputs.pr_number }}
|
||||
docker-compose pull
|
||||
docker-compose up -d
|
||||
ENDSSH
|
||||
|
||||
- name: Post deployment URL to PR
|
||||
if: success()
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const { GITHUB_REPOSITORY } = process.env;
|
||||
const [repoOwner, repoName] = GITHUB_REPOSITORY.split('/');
|
||||
const prNumber = ${{ needs.check-comment.outputs.pr_number }};
|
||||
|
||||
const deploymentUrl = `http://${{ secrets.VPS_HOST }}:${prNumber}`;
|
||||
const commentBody = `## 🚀 PR Test Deployment\n\n` +
|
||||
`Your PR has been deployed for testing!\n\n` +
|
||||
`🔗 **Test URL:** [${deploymentUrl}](${deploymentUrl})\n\n` +
|
||||
`This deployment will be automatically cleaned up when the PR is closed.\n\n`;
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner: repoOwner,
|
||||
repo: repoName,
|
||||
issue_number: prNumber,
|
||||
body: commentBody
|
||||
});
|
||||
78
.github/workflows/PR-Demo-cleanup.yml
vendored
Normal file
78
.github/workflows/PR-Demo-cleanup.yml
vendored
Normal file
@@ -0,0 +1,78 @@
|
||||
name: PR Deployment cleanup
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, closed]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
env:
|
||||
SERVER_IP: ${{ secrets.VPS_IP }} # Add this to your GitHub secrets
|
||||
CLEANUP_PERFORMED: 'false' # Add flag to track if cleanup occurred
|
||||
|
||||
jobs:
|
||||
cleanup:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.action == 'closed'
|
||||
|
||||
steps:
|
||||
- name: Set up SSH
|
||||
run: |
|
||||
mkdir -p ~/.ssh/
|
||||
echo "${{ secrets.VPS_SSH_KEY }}" > ../private.key
|
||||
sudo chmod 600 ../private.key
|
||||
|
||||
- name: Cleanup PR deployment
|
||||
id: cleanup
|
||||
run: |
|
||||
CLEANUP_STATUS=$(ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }} << 'ENDSSH'
|
||||
if [ -d "/stirling/PR-${{ github.event.pull_request.number }}" ]; then
|
||||
echo "Found PR directory, proceeding with cleanup..."
|
||||
|
||||
# Stop and remove containers
|
||||
cd /stirling/PR-${{ github.event.pull_request.number }}
|
||||
docker-compose down || true
|
||||
|
||||
# Go back to root before removal
|
||||
cd /
|
||||
|
||||
# Remove PR-specific directories
|
||||
rm -rf /stirling/PR-${{ github.event.pull_request.number }}
|
||||
|
||||
# Remove the Docker image
|
||||
docker rmi --no-prune ${{ secrets.DOCKER_HUB_USERNAME }}/test:pr-${{ github.event.pull_request.number }} || true
|
||||
|
||||
echo "PERFORMED_CLEANUP"
|
||||
else
|
||||
echo "PR directory not found, nothing to clean up"
|
||||
echo "NO_CLEANUP_NEEDED"
|
||||
fi
|
||||
ENDSSH
|
||||
)
|
||||
|
||||
if [[ $CLEANUP_STATUS == *"PERFORMED_CLEANUP"* ]]; then
|
||||
echo "cleanup_performed=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "cleanup_performed=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Post cleanup notice to PR
|
||||
if: steps.cleanup.outputs.cleanup_performed == 'true'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const { GITHUB_REPOSITORY } = process.env;
|
||||
const [repoOwner, repoName] = GITHUB_REPOSITORY.split('/');
|
||||
const prNumber = context.issue.number;
|
||||
|
||||
const commentBody = `## 🧹 Deployment Cleanup\n\n` +
|
||||
`The test deployment for this PR has been cleaned up.`;
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner: repoOwner,
|
||||
repo: repoName,
|
||||
issue_number: prNumber,
|
||||
body: commentBody
|
||||
});
|
||||
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@@ -76,7 +76,7 @@ jobs:
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.7"
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Pip requirements
|
||||
run: |
|
||||
|
||||
119
.github/workflows/check_properties.yml
vendored
119
.github/workflows/check_properties.yml
vendored
@@ -6,18 +6,22 @@ on:
|
||||
paths:
|
||||
- "src/main/resources/messages_*.properties"
|
||||
push:
|
||||
branches: ["main"]
|
||||
paths:
|
||||
- "src/main/resources/messages_en_GB.properties"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
check-files:
|
||||
if: github.event_name == 'pull_request_target'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout main branch first
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: main
|
||||
path: main-branch
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Checkout PR branch
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -26,13 +30,6 @@ jobs:
|
||||
path: pr-branch
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Checkout main branch
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: main
|
||||
path: main-branch
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
@@ -49,56 +46,73 @@ jobs:
|
||||
echo "Fetching PR changed files..."
|
||||
cd pr-branch
|
||||
gh repo set-default ${{ github.repository }}
|
||||
gh pr view ${{ github.event.pull_request.number }} --json files -q ".files[].path" > ../changed_files.txt
|
||||
# Store files in a safe way, only allowing valid properties files
|
||||
echo "Getting list of changed files from PR..."
|
||||
gh pr view ${{ github.event.pull_request.number }} --json files -q ".files[].path" | grep -E '^src/main/resources/messages_[a-zA-Z_]+\.properties$' > ../changed_files.txt
|
||||
cd ..
|
||||
echo $(cat changed_files.txt)
|
||||
BRANCH_PATH="pr-branch"
|
||||
echo "BRANCH_PATH=${BRANCH_PATH}" >> $GITHUB_ENV
|
||||
CHANGED_FILES=$(cat changed_files.txt | tr '\n' ' ')
|
||||
echo "CHANGED_FILES=${CHANGED_FILES}" >> $GITHUB_ENV
|
||||
echo "Changed files: ${CHANGED_FILES}"
|
||||
echo "Branch: ${BRANCH_PATH}"
|
||||
|
||||
echo "Processing changed files..."
|
||||
mapfile -t CHANGED_FILES < changed_files.txt
|
||||
|
||||
CHANGED_FILES_STR="${CHANGED_FILES[*]}"
|
||||
echo "CHANGED_FILES=${CHANGED_FILES_STR}" >> $GITHUB_ENV
|
||||
|
||||
echo "Changed files: ${CHANGED_FILES_STR}"
|
||||
|
||||
- name: Determine reference file
|
||||
id: determine-file
|
||||
run: |
|
||||
echo "Determining reference file..."
|
||||
if echo "${{ env.CHANGED_FILES }}" | grep -q 'src/main/resources/messages_en_GB.properties'; then
|
||||
if grep -Fxq "src/main/resources/messages_en_GB.properties" changed_files.txt; then
|
||||
echo "Using PR branch reference file"
|
||||
echo "REFERENCE_FILE=pr-branch/src/main/resources/messages_en_GB.properties" >> $GITHUB_ENV
|
||||
else
|
||||
echo "Using main branch reference file"
|
||||
echo "REFERENCE_FILE=main-branch/src/main/resources/messages_en_GB.properties" >> $GITHUB_ENV
|
||||
fi
|
||||
echo "REFERENCE_FILE=${{ env.REFERENCE_FILE }}"
|
||||
|
||||
- name: Show REFERENCE_FILE
|
||||
run: echo "Reference file is set to ${{ env.REFERENCE_FILE }}"
|
||||
run: echo "Reference file is set to ${REFERENCE_FILE}"
|
||||
|
||||
- name: Run Python script to check files
|
||||
id: run-check
|
||||
run: |
|
||||
python main-branch/.github/scripts/check_language_properties.py --reference-file ${{ env.REFERENCE_FILE }} --branch ${{ env.BRANCH_PATH }} --files ${{ env.CHANGED_FILES }} > failure.txt || true
|
||||
echo "Running Python script to check files..."
|
||||
python main-branch/.github/scripts/check_language_properties.py \
|
||||
--actor ${{ github.event.pull_request.user.login }} \
|
||||
--reference-file "${REFERENCE_FILE}" \
|
||||
--branch pr-branch \
|
||||
--files "${CHANGED_FILES[@]}" > result.txt || true
|
||||
|
||||
- name: Capture output
|
||||
id: capture-output
|
||||
run: |
|
||||
if [ -f failure.txt ] && [ -s failure.txt ]; then
|
||||
echo "Test failed, capturing output..."
|
||||
ERROR_OUTPUT=$(cat failure.txt)
|
||||
echo "ERROR_OUTPUT<<EOF" >> $GITHUB_ENV
|
||||
echo "$ERROR_OUTPUT" >> $GITHUB_ENV
|
||||
if [ -f result.txt ] && [ -s result.txt ]; then
|
||||
echo "Test, capturing output..."
|
||||
SCRIPT_OUTPUT=$(cat result.txt)
|
||||
echo "SCRIPT_OUTPUT<<EOF" >> $GITHUB_ENV
|
||||
echo "$SCRIPT_OUTPUT" >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
echo $ERROR_OUTPUT
|
||||
echo "${SCRIPT_OUTPUT}"
|
||||
|
||||
# Set FAIL_JOB to true if SCRIPT_OUTPUT contains ❌
|
||||
if [[ "$SCRIPT_OUTPUT" == *"❌"* ]]; then
|
||||
echo "FAIL_JOB=true" >> $GITHUB_ENV
|
||||
else
|
||||
echo "FAIL_JOB=false" >> $GITHUB_ENV
|
||||
fi
|
||||
else
|
||||
echo "No errors found."
|
||||
echo "ERROR_OUTPUT=" >> $GITHUB_ENV
|
||||
echo "No update found."
|
||||
echo "SCRIPT_OUTPUT=" >> $GITHUB_ENV
|
||||
echo "FAIL_JOB=false" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Post comment on PR
|
||||
if: env.ERROR_OUTPUT != ''
|
||||
if: env.SCRIPT_OUTPUT != ''
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const { GITHUB_REPOSITORY, ERROR_OUTPUT } = process.env;
|
||||
const { GITHUB_REPOSITORY, SCRIPT_OUTPUT } = process.env;
|
||||
const [repoOwner, repoName] = GITHUB_REPOSITORY.split('/');
|
||||
const prNumber = context.issue.number;
|
||||
|
||||
@@ -120,7 +134,7 @@ jobs:
|
||||
owner: repoOwner,
|
||||
repo: repoName,
|
||||
comment_id: comment.id,
|
||||
body: `## 🚀 Translation Verification Summary\n\n\n${ERROR_OUTPUT}\n`
|
||||
body: `## 🚀 Translation Verification Summary\n\n\n${SCRIPT_OUTPUT}\n`
|
||||
});
|
||||
console.log("Updated existing comment.");
|
||||
} else if (!comment) {
|
||||
@@ -129,33 +143,24 @@ jobs:
|
||||
owner: repoOwner,
|
||||
repo: repoName,
|
||||
issue_number: prNumber,
|
||||
body: `## 🚀 Translation Verification Summary\n\n\n${ERROR_OUTPUT}\n`
|
||||
body: `## 🚀 Translation Verification Summary\n\n\n${SCRIPT_OUTPUT}\n`
|
||||
});
|
||||
console.log("Created new comment.");
|
||||
} else {
|
||||
console.log("Comment update attempt denied. Actor does not match.");
|
||||
}
|
||||
|
||||
# - name: Set up git config
|
||||
# run: |
|
||||
# git config --global user.name "github-actions[bot]"
|
||||
# git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
# - name: Add translation keys
|
||||
# run: |
|
||||
# cd ${{ env.BRANCH_PATH }}
|
||||
# git add src/main/resources/messages_*.properties
|
||||
# git diff --staged --quiet || echo "CHANGES_DETECTED=true" >> $GITHUB_ENV
|
||||
# git commit -m "Update translation files" || echo "No changes to commit"
|
||||
# - name: Push
|
||||
# if: env.CHANGES_DETECTED == 'true'
|
||||
# run: |
|
||||
# cd pr-branch
|
||||
# git remote set-url origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.event.pull_request.head.repo.full_name }}.git
|
||||
# git push origin ${{ github.head_ref }} || echo "Push failed: possibly no changes to push"
|
||||
- name: Fail job if errors found
|
||||
if: env.FAIL_JOB == 'true'
|
||||
run: |
|
||||
echo "Failing the job because errors were detected."
|
||||
exit 1
|
||||
|
||||
update-translations-main:
|
||||
if: github.event_name == 'push'
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
@@ -169,7 +174,10 @@ jobs:
|
||||
- name: Run Python script to check files
|
||||
id: run-check
|
||||
run: |
|
||||
python .github/scripts/check_language_properties.py --reference-file src/main/resources/messages_en_GB.properties --branch main
|
||||
echo "Running Python script to check files..."
|
||||
python .github/scripts/check_language_properties.py \
|
||||
--reference-file src/main/resources/messages_en_GB.properties \
|
||||
--branch main
|
||||
|
||||
- name: Set up git config
|
||||
run: |
|
||||
@@ -184,7 +192,7 @@ jobs:
|
||||
- name: Create Pull Request
|
||||
id: cpr
|
||||
if: env.CHANGES_DETECTED == 'true'
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
commit-message: "Update translation files"
|
||||
@@ -193,6 +201,8 @@ jobs:
|
||||
signoff: true
|
||||
branch: update_translation_files
|
||||
title: "Update translation files"
|
||||
add-paths: |
|
||||
src/main/resources/messages_*.properties
|
||||
body: |
|
||||
Auto-generated by [create-pull-request][1]
|
||||
|
||||
@@ -200,3 +210,4 @@ jobs:
|
||||
labels: Translation
|
||||
draft: false
|
||||
delete-branch: true
|
||||
sign-commits: true
|
||||
|
||||
96
.github/workflows/multiOSReleases.yml
vendored
Normal file
96
.github/workflows/multiOSReleases.yml
vendored
Normal file
@@ -0,0 +1,96 @@
|
||||
name: Test Installers Build
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
release:
|
||||
types: [created]
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
jobs:
|
||||
build-installers:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- os: windows-latest
|
||||
platform: win
|
||||
ext: exe
|
||||
#- os: macos-latest
|
||||
# platform: mac
|
||||
# ext: dmg
|
||||
#- os: ubuntu-latest
|
||||
# platform: linux
|
||||
# ext: deb
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: "21"
|
||||
distribution: "temurin"
|
||||
|
||||
- uses: gradle/actions/setup-gradle@v4
|
||||
with:
|
||||
gradle-version: 8.7
|
||||
|
||||
# Install Windows dependencies
|
||||
- name: Install WiX Toolset
|
||||
if: matrix.os == 'windows-latest'
|
||||
run: |
|
||||
curl -L -o wix.exe https://github.com/wixtoolset/wix3/releases/download/wix3141rtm/wix314.exe
|
||||
.\wix.exe /install /quiet
|
||||
|
||||
# Install Linux dependencies
|
||||
- name: Install Linux Dependencies
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y fakeroot rpm
|
||||
|
||||
# Get version number
|
||||
- name: Get version number
|
||||
id: versionNumber
|
||||
run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT
|
||||
shell: bash
|
||||
|
||||
- name: Get version number mac
|
||||
id: versionNumberMac
|
||||
run: echo "versionNumberMac=$(./gradlew printMacVersion --quiet | tail -1)" >> $GITHUB_OUTPUT
|
||||
shell: bash
|
||||
|
||||
# Build installer
|
||||
- name: Build Installer
|
||||
run: ./gradlew build jpackage -x test --info
|
||||
env:
|
||||
DOCKER_ENABLE_SECURITY: false
|
||||
STIRLING_PDF_DESKTOP_UI: true
|
||||
|
||||
# Rename and collect artifacts based on OS
|
||||
- name: Prepare artifacts
|
||||
id: prepare
|
||||
shell: bash
|
||||
run: |
|
||||
if [ "${{ matrix.os }}" = "windows-latest" ]; then
|
||||
mv "build/jpackage/Stirling-PDF-${{ steps.versionNumber.outputs.versionNumber }}.exe" "Stirling-PDF-${{ matrix.platform }}-installer.${{ matrix.ext }}"
|
||||
elif [ "${{ matrix.os }}" = "macos-latest" ]; then
|
||||
mv "build/jpackage/Stirling-PDF-${{ steps.versionNumberMac.outputs.versionNumberMac }}.dmg" "Stirling-PDF-${{ steps.versionNumber.outputs.versionNumber }}-${{ matrix.platform }}.${{ matrix.ext }}"
|
||||
else
|
||||
mv "build/jpackage/stirling-pdf_${{ steps.versionNumber.outputs.versionNumber }}-1_amd64.deb" "Stirling-PDF-${{ steps.versionNumber.outputs.versionNumber }}-${{ matrix.platform }}.${{ matrix.ext }}"
|
||||
fi
|
||||
|
||||
# Upload installer as artifact for testing
|
||||
- name: Upload Installer Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Stirling-PDF-${{ matrix.platform }}-installer.${{ matrix.ext }}
|
||||
path: Stirling-PDF-${{ matrix.platform }}-installer.${{ matrix.ext }}
|
||||
retention-days: 1
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload binaries to release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: ./Stirling-PDF-${{ matrix.platform }}-installer.${{ matrix.ext }}
|
||||
3
.github/workflows/push-docker.yml
vendored
3
.github/workflows/push-docker.yml
vendored
@@ -67,6 +67,7 @@ jobs:
|
||||
images: |
|
||||
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
|
||||
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/s-pdf
|
||||
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf
|
||||
${{ secrets.DOCKER_HUB_ORG_USERNAME }}/stirling-pdf
|
||||
tags: |
|
||||
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }},enable=${{ github.ref == 'refs/heads/master' }}
|
||||
@@ -95,6 +96,7 @@ jobs:
|
||||
images: |
|
||||
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
|
||||
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/s-pdf
|
||||
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf
|
||||
${{ secrets.DOCKER_HUB_ORG_USERNAME }}/stirling-pdf
|
||||
tags: |
|
||||
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}-ultra-lite,enable=${{ github.ref == 'refs/heads/master' }}
|
||||
@@ -122,6 +124,7 @@ jobs:
|
||||
images: |
|
||||
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
|
||||
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/s-pdf
|
||||
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf
|
||||
${{ secrets.DOCKER_HUB_ORG_USERNAME }}/stirling-pdf
|
||||
tags: |
|
||||
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}-fat,enable=${{ github.ref == 'refs/heads/master' }}
|
||||
|
||||
11
.github/workflows/releaseArtifacts.yml
vendored
11
.github/workflows/releaseArtifacts.yml
vendored
@@ -35,27 +35,28 @@ jobs:
|
||||
run: ./gradlew clean createExe
|
||||
env:
|
||||
DOCKER_ENABLE_SECURITY: ${{ matrix.enable_security }}
|
||||
STIRLING_PDF_DESKTOP_UI: false
|
||||
|
||||
- name: Get version number
|
||||
id: versionNumber
|
||||
run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Rename binarie
|
||||
if: matrix.file_suffix != ''
|
||||
run: cp ./build/launch4j/Stirling-PDF.exe ./build/launch4j/Stirling-PDF${{ matrix.file_suffix }}.exe
|
||||
run: cp ./build/launch4j/Stirling-PDF.exe ./build/launch4j/Stirling-PDF-Server${{ matrix.file_suffix }}.exe
|
||||
|
||||
- name: Upload Assets binarie
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
path: ./build/launch4j/Stirling-PDF${{ matrix.file_suffix }}.exe
|
||||
name: Stirling-PDF${{ matrix.file_suffix }}.exe
|
||||
path: ./build/launch4j/Stirling-PDF-Server${{ matrix.file_suffix }}.exe
|
||||
name: Stirling-PDF-Server${{ 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
|
||||
files: ./build/launch4j/Stirling-PDF-Server${{ 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
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -161,3 +161,4 @@ out/
|
||||
.pytest_cache
|
||||
.ipynb_checkpoints
|
||||
|
||||
**/jcef-bundle/
|
||||
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@@ -49,5 +49,7 @@
|
||||
"editor.indentSize": "tabSize",
|
||||
"editor.stickyScroll.enabled": false,
|
||||
"editor.minimap.enabled": false,
|
||||
"editor.formatOnSave": true
|
||||
"editor.formatOnSave": true,
|
||||
"java.format.settings.google.mode": "jar-file",
|
||||
"java.format.settings.google.extra": "--aosp --skip-sorting-imports"
|
||||
}
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
# New Database Backup and Import Functionality
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Full activation will take place on approximately January 5th, 2025!**
|
||||
|
||||
Why is the waiting time six months?
|
||||
|
||||
There are users who only install updates sporadically; if they skip the preparation, it can/will lead to data loss in the database.
|
||||
|
||||
## Functionality Overview
|
||||
|
||||
The newly introduced feature enhances the application with robust database backup and import capabilities. This feature is designed to ensure data integrity and provide a straightforward way to manage database backups. Here's how it works:
|
||||
|
||||
@@ -11,7 +11,7 @@ Stirling-PDF is built using:
|
||||
- Spring Boot + Thymeleaf
|
||||
- PDFBox
|
||||
- LibreOffice
|
||||
- OcrMyPdf
|
||||
- qpdf
|
||||
- HTML, CSS, JavaScript
|
||||
- Docker
|
||||
- PDF.js
|
||||
@@ -114,7 +114,7 @@ These files provide pre-configured setups for different scenarios. For example,
|
||||
services:
|
||||
stirling-pdf:
|
||||
container_name: Stirling-PDF-Security
|
||||
image: frooodle/s-pdf:latest
|
||||
image: stirlingtools/stirling-pdf:latest
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
@@ -173,20 +173,20 @@ Stirling-PDF uses different Docker images for various configurations. The build
|
||||
For the latest version:
|
||||
|
||||
```bash
|
||||
docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t frooodle/s-pdf:latest -f ./Dockerfile .
|
||||
docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t stirlingtools/stirling-pdf:latest -f ./Dockerfile .
|
||||
```
|
||||
|
||||
For the ultra-lite version:
|
||||
|
||||
```bash
|
||||
docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t frooodle/s-pdf:latest-ultra-lite -f ./Dockerfile-ultra-lite .
|
||||
docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t stirlingtools/stirling-pdf:latest-ultra-lite -f ./Dockerfile-ultra-lite .
|
||||
```
|
||||
|
||||
For the fat version (with security enabled):
|
||||
|
||||
```bash
|
||||
export DOCKER_ENABLE_SECURITY=true
|
||||
docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t frooodle/s-pdf:latest-fat -f ./Dockerfile-fat .
|
||||
docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t stirlingtools/stirling-pdf:latest-fat -f ./Dockerfile-fat .
|
||||
```
|
||||
|
||||
Note: The `--no-cache` and `--pull` flags ensure that the build process uses the latest base images and doesn't use cached layers, which is useful for testing and ensuring reproducible builds. however to improve build times these can often be removed depending on your usecase
|
||||
@@ -243,7 +243,7 @@ To run Stirling-PDF locally:
|
||||
|
||||
Important notes:
|
||||
|
||||
- Local testing doesn't include features that depend on external tools like OCRmyPDF, LibreOffice, or Python scripts.
|
||||
- Local testing doesn't include features that depend on external tools like qpdf, LibreOffice, or Python scripts.
|
||||
- There are currently no automated unit tests. All testing is done manually through the UI or API calls. (You are welcome to add JUnits!)
|
||||
- Always verify your changes in the full Docker environment before submitting pull requests, as some integrations and features will only work in the complete setup.
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /et
|
||||
tini \
|
||||
bash \
|
||||
curl \
|
||||
qpdf \
|
||||
shadow \
|
||||
su-exec \
|
||||
openssl \
|
||||
@@ -40,7 +41,6 @@ RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /et
|
||||
# pdftohtml
|
||||
poppler-utils \
|
||||
# OCR MY PDF (unpaper for descew and other advanced features)
|
||||
ocrmypdf \
|
||||
tesseract-ocr-data-eng \
|
||||
# CV
|
||||
py3-opencv \
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Build the application
|
||||
FROM gradle:8.7-jdk17 AS build
|
||||
FROM gradle:8.11-jdk17 AS build
|
||||
|
||||
# Set the working directory
|
||||
WORKDIR /app
|
||||
@@ -55,7 +55,7 @@ RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /et
|
||||
# pdftohtml
|
||||
poppler-utils \
|
||||
# OCR MY PDF (unpaper for descew and other advanced featues)
|
||||
ocrmypdf \
|
||||
qpdf \
|
||||
tesseract-ocr-data-eng \
|
||||
font-terminus font-dejavu font-noto font-noto-cjk font-awesome font-noto-extra \
|
||||
# CV
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# use alpine
|
||||
FROM alpine:3.20.3
|
||||
FROM alpine:3.21.0
|
||||
|
||||
ARG VERSION_TAG
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
| Operation | PageOps | Convert | Security | Other | CLI | Python | OpenCV | LibreOffice | OCRmyPDF | Java | Javascript | Unoconv | Ghostscript |
|
||||
| Operation | PageOps | Convert | Security | Other | CLI | Python | OpenCV | LibreOffice | qpdf | Java | Javascript | Unoconv | tesseract |
|
||||
| ------------------- | ------- | ------- | -------- | ----- | --- | ------ | ------ | ----------- | -------- | ---- | ---------- | ------- | ----------- |
|
||||
| adjust-contrast | ✔️ | | | | | | | | | | ✔️ | | |
|
||||
| auto-split-pdf | ✔️ | | | | | | | | | ✔️ | | | |
|
||||
@@ -16,7 +16,7 @@
|
||||
| img-to-pdf | | ✔️ | | | | | | | | ✔️ | | | |
|
||||
| pdf-to-html | | ✔️ | | | ✔️ | | | ✔️ | | | | | |
|
||||
| pdf-to-img | | ✔️ | | | | ✔️ | | | | ✔️ | | | |
|
||||
| pdf-to-pdfa | | ✔️ | | | ✔️ | | | | ✔️ | | | | ✔️ |
|
||||
| pdf-to-pdfa | | ✔️ | | | ✔️ | | | | ✔️ | | | | |
|
||||
| pdf-to-markdown | | ✔️ | | | | | | | | ✔️ | | | |
|
||||
| pdf-to-presentation | | ✔️ | | | ✔️ | | | ✔️ | | | | | |
|
||||
| pdf-to-text | | ✔️ | | | ✔️ | | | ✔️ | | | | | |
|
||||
@@ -34,13 +34,13 @@
|
||||
| auto-rename | | | | ✔️ | | | | | | ✔️ | | | |
|
||||
| change-metadata | | | | ✔️ | | | | | | ✔️ | | | |
|
||||
| compare | | | | ✔️ | | | | | | | ✔️ | | |
|
||||
| compress-pdf | | | | ✔️ | ✔️ | | | | ✔️ | | | | ✔️ |
|
||||
| compress-pdf | | | | ✔️ | ✔️ | | | | ✔️ | | | | |
|
||||
| extract-image-scans | | | | ✔️ | ✔️ | ✔️ | ✔️ | | | | | | |
|
||||
| extract-images | | | | ✔️ | | | | | | ✔️ | | | |
|
||||
| flatten | | | | ✔️ | | | | | | | ✔️ | | |
|
||||
| get-info-on-pdf | | | | ✔️ | | | | | | ✔️ | | | |
|
||||
| ocr-pdf | | | | ✔️ | ✔️ | | | | ✔️ | | | | |
|
||||
| ocr-pdf | | | | ✔️ | ✔️ | | | | | | | | ✔ |
|
||||
| remove-blanks | | | | ✔️ | ✔️ | ✔️ | ✔️ | | | | | | |
|
||||
| repair | | | | ✔️ | ✔️ | | | ✔️ | | | | | ✔️ |
|
||||
| repair | | | | ✔️ | ✔️ | | | ✔️ | ✔ | | | | |
|
||||
| show-javascript | | | | ✔️ | | | | | | | ✔️ | | |
|
||||
| sign | | | | ✔️ | | | | | | | ✔️ | | |
|
||||
|
||||
@@ -8,7 +8,7 @@ The paths have changed for the tessdata locations on new Docker images. Please u
|
||||
|
||||
## How does the OCR Work
|
||||
|
||||
Stirling-PDF uses [OCRmyPDF](https://github.com/ocrmypdf/OCRmyPDF), which in turn uses Tesseract for its text recognition. All credit goes to them for this awesome work!
|
||||
Stirling-PDF uses Tesseract for its text recognition. All credit goes to them for this awesome work!
|
||||
|
||||
## Language Packs
|
||||
|
||||
@@ -52,8 +52,6 @@ Add the following to your existing Docker run command:
|
||||
|
||||
### Non-Docker Setup
|
||||
|
||||
If you are not using Docker, you need to install the OCR components, including the `ocrmypdf` app. You can see the [OCRmyPDF install guide](https://ocrmypdf.readthedocs.io/en/latest/installation.html).
|
||||
|
||||
For Debian-based systems, install languages with this command:
|
||||
|
||||
```bash
|
||||
@@ -83,8 +81,7 @@ rpm -qa | grep tesseract-langpack | sed 's/tesseract-langpack-//g'
|
||||
|
||||
For Windows:
|
||||
|
||||
Ensure ocrmypdf in installed with
|
||||
``pip install ocrmypdf``
|
||||
You must ensure tesseract is installed
|
||||
|
||||
Additional languages must be downloaded manually:
|
||||
Download desired .traineddata files from tessdata or tessdata_fast
|
||||
|
||||
45
Jenkinsfile
vendored
45
Jenkinsfile
vendored
@@ -1,45 +0,0 @@
|
||||
pipeline {
|
||||
agent any
|
||||
stages {
|
||||
stage('Build') {
|
||||
steps {
|
||||
sh 'chmod 755 gradlew'
|
||||
sh './gradlew build'
|
||||
}
|
||||
}
|
||||
stage('Docker Build') {
|
||||
steps {
|
||||
script {
|
||||
def appVersion = sh(returnStdout: true, script: './gradlew printVersion -q').trim()
|
||||
def image = "frooodle/s-pdf:$appVersion"
|
||||
sh "docker build -t $image ."
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Docker Push') {
|
||||
steps {
|
||||
script {
|
||||
def appVersion = sh(returnStdout: true, script: './gradlew printVersion -q').trim()
|
||||
def image = "frooodle/s-pdf:$appVersion"
|
||||
withCredentials([string(credentialsId: 'docker_hub_access_token', variable: 'DOCKER_HUB_ACCESS_TOKEN')]) {
|
||||
sh "docker login --username frooodle --password $DOCKER_HUB_ACCESS_TOKEN"
|
||||
sh "docker push $image"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Helm Push') {
|
||||
steps {
|
||||
script {
|
||||
//TODO: Read chartVersion from Chart.yaml
|
||||
def chartVersion = '1.0.0'
|
||||
withCredentials([string(credentialsId: 'docker_hub_access_token', variable: 'DOCKER_HUB_ACCESS_TOKEN')]) {
|
||||
sh "docker login --username frooodle --password $DOCKER_HUB_ACCESS_TOKEN"
|
||||
sh "helm package chart/stirling-pdf"
|
||||
sh "helm push stirling-pdf-chart-1.0.0.tgz oci://registry-1.docker.io/frooodle"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -68,7 +68,7 @@ nix-env -iA nixpkgs.jbig2enc
|
||||
|
||||
### Step 3: Install Additional Software
|
||||
|
||||
Next we need to install LibreOffice for conversions, ocrmypdf for OCR, and OpenCV for pattern recognition functionality.
|
||||
Next we need to install LibreOffice for conversions, qpdf for OCR, and OpenCV for pattern recognition functionality.
|
||||
|
||||
Install the following software:
|
||||
|
||||
@@ -81,27 +81,27 @@ Install the following software:
|
||||
- unoconv
|
||||
- pngquant
|
||||
- unpaper
|
||||
- ocrmypdf
|
||||
- qpdf
|
||||
- opencv-python-headless
|
||||
|
||||
For Debian-based systems, you can use the following command:
|
||||
|
||||
```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 qpdf
|
||||
pip3 install uno opencv-python-headless unoconv pngquant WeasyPrint --break-system-packages
|
||||
```
|
||||
|
||||
For Fedora:
|
||||
|
||||
```bash
|
||||
sudo dnf install -y libreoffice-writer libreoffice-calc libreoffice-impress unpaper ocrmypdf
|
||||
sudo dnf install -y libreoffice-writer libreoffice-calc libreoffice-impress unpaper qpdf
|
||||
pip3 install uno opencv-python-headless unoconv pngquant WeasyPrint
|
||||
```
|
||||
|
||||
For Nix:
|
||||
|
||||
```bash
|
||||
nix-env -iA nixpkgs.unpaper nixpkgs.libreoffice nixpkgs.ocrmypdf nixpkgs.poppler_utils
|
||||
nix-env -iA nixpkgs.unpaper nixpkgs.libreoffice nixpkgs.qpdf nixpkgs.poppler_utils
|
||||
pip3 install uno opencv-python-headless unoconv pngquant WeasyPrint
|
||||
```
|
||||
|
||||
@@ -146,7 +146,6 @@ The easiest method is to use the language packs provided by your repositories. S
|
||||
|
||||
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/tessdata`
|
||||
3. Please view [OCRmyPDF install guide](https://ocrmypdf.readthedocs.io/en/latest/installation.html) for more info.
|
||||
|
||||
**IMPORTANT:** DO NOT REMOVE EXISTING `eng.traineddata`, IT'S REQUIRED.
|
||||
|
||||
|
||||
99
README.md
99
README.md
@@ -2,10 +2,11 @@
|
||||
<h1 align="center">Stirling-PDF</h1>
|
||||
|
||||
[](https://hub.docker.com/r/frooodle/s-pdf)
|
||||
[](https://discord.gg/Cn8pWhQRxZ)
|
||||
[](https://discord.gg/HYmhKj45pU)
|
||||
[](https://github.com/Stirling-Tools/Stirling-PDF/)
|
||||
[](https://github.com/Stirling-Tools/stirling-pdf)
|
||||
|
||||
<a href="https://www.producthunt.com/posts/stirling-pdf?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-stirling-pdf" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=641239&theme=light" alt="Stirling PDF - Open source locally hosted web PDF editor | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
[](https://cloud.digitalocean.com/apps/new?repo=https://github.com/Stirling-Tools/Stirling-PDF/tree/digitalOcean&refcode=c3210994b1af)
|
||||
|
||||
[Stirling-PDF](https://www.stirlingpdf.com) 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.
|
||||
@@ -18,7 +19,7 @@ All files and PDFs exist either exclusively on the client side, reside in server
|
||||
|
||||
## Features
|
||||
|
||||
- Enterprise features like SSO Check [here](https://docs.stirlingpdf.com/Enterprise%20Edition)
|
||||
- Enterprise features like SSO Check [here](https://docs.stirlingpdf.com/Enterprise%20Edition)
|
||||
- Dark mode support
|
||||
- Custom download options
|
||||
- Parallel file processing and downloads
|
||||
@@ -78,15 +79,15 @@ All files and PDFs exist either exclusively on the client side, reside in server
|
||||
- Detect and remove blank pages
|
||||
- Compare two PDFs and show differences in text
|
||||
- Add images to PDFs
|
||||
- Compress PDFs to decrease their filesize (using OCRMyPDF)
|
||||
- Compress PDFs to decrease their filesize (using qpdf)
|
||||
- Extract images from PDF
|
||||
- Remove images from PDF
|
||||
- Extract images from scans
|
||||
- Remove annotations
|
||||
- Add page numbers
|
||||
- Auto rename file by detecting PDF header text
|
||||
- OCR on PDF (using OCRMyPDF)
|
||||
- PDF/A conversion (using OCRMyPDF)
|
||||
- OCR on PDF (using tesseract)
|
||||
- PDF/A conversion (using libreoffice)
|
||||
- Edit metadata
|
||||
- Flatten PDFs
|
||||
- Get all information on a PDF to view or export as JSON
|
||||
@@ -101,7 +102,7 @@ A demo of the app is available [here](https://stirlingpdf.io).
|
||||
- Spring Boot + Thymeleaf
|
||||
- [PDFBox](https://github.com/apache/pdfbox/tree/trunk)
|
||||
- [LibreOffice](https://www.libreoffice.org/discover/libreoffice/) for advanced conversions
|
||||
- [OcrMyPdf](https://github.com/ocrmypdf/OCRmyPDF)
|
||||
- [qpdf](https://github.com/qpdf/qpdf)
|
||||
- HTML, CSS, JavaScript
|
||||
- Docker
|
||||
- [PDF.js](https://github.com/mozilla/pdf.js)
|
||||
@@ -120,13 +121,13 @@ Please view the [LocalRunGuide](https://github.com/Stirling-Tools/Stirling-PDF/b
|
||||
### Docker / Podman
|
||||
|
||||
> [!NOTE]
|
||||
> <https://hub.docker.com/r/frooodle/s-pdf>
|
||||
> <https://hub.docker.com/r/stirlingtools/stirling-pdf>
|
||||
|
||||
Stirling-PDF has three different versions: a full version, an ultra-lite version, and a 'fat' 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). For people that don't mind space optimization, just use the latest tag.
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
Please note in the examples below, you may need to change the volume paths as needed, e.g., `./extraConfigs:/configs` to `/opt/stirlingpdf/extraConfigs:/configs`.
|
||||
|
||||
@@ -144,7 +145,7 @@ docker run -d \
|
||||
-e INSTALL_BOOK_AND_ADVANCED_HTML_OPS=false \
|
||||
-e LANGS=en_GB \
|
||||
--name stirling-pdf \
|
||||
frooodle/s-pdf:latest
|
||||
stirlingtools/stirling-pdf:latest
|
||||
```
|
||||
|
||||
### Docker Compose
|
||||
@@ -153,7 +154,7 @@ docker run -d \
|
||||
version: '3.3'
|
||||
services:
|
||||
stirling-pdf:
|
||||
image: frooodle/s-pdf:latest
|
||||
image: stirlingtools/stirling-pdf:latest
|
||||
ports:
|
||||
- '8080:8080'
|
||||
volumes:
|
||||
@@ -186,46 +187,48 @@ Certain functionality like `Sign` supports pre-saved files stored at `/customFil
|
||||
|
||||
## Supported Languages
|
||||
|
||||
Stirling-PDF currently supports 36 languages!
|
||||
Stirling-PDF currently supports 38 languages!
|
||||
|
||||
| Language | Progress |
|
||||
| -------------------------------------------- | -------------------------------------- |
|
||||
| Arabic (العربية) (ar_AR) |  |
|
||||
| Basque (Euskara) (eu_ES) |  |
|
||||
| Bulgarian (Български) (bg_BG) |  |
|
||||
| Catalan (Català) (ca_CA) |  |
|
||||
| Croatian (Hrvatski) (hr_HR) |  |
|
||||
| Czech (Česky) (cs_CZ) |  |
|
||||
| Danish (Dansk) (da_DK) |  |
|
||||
| Dutch (Nederlands) (nl_NL) |  |
|
||||
| Arabic (العربية) (ar_AR) |  |
|
||||
| Azerbaijani (Azərbaycan Dili) (az_AZ) |  |
|
||||
| Basque (Euskara) (eu_ES) |  |
|
||||
| Bulgarian (Български) (bg_BG) |  |
|
||||
| Catalan (Català) (ca_CA) |  |
|
||||
| Croatian (Hrvatski) (hr_HR) |  |
|
||||
| Czech (Česky) (cs_CZ) |  |
|
||||
| Danish (Dansk) (da_DK) |  |
|
||||
| Dutch (Nederlands) (nl_NL) |  |
|
||||
| English (English) (en_GB) |  |
|
||||
| English (US) (en_US) |  |
|
||||
| French (Français) (fr_FR) |  |
|
||||
| German (Deutsch) (de_DE) |  |
|
||||
| Greek (Ελληνικά) (el_GR) |  |
|
||||
| Hindi (हिंदी) (hi_IN) |  |
|
||||
| Hungarian (Magyar) (hu_HU) |  |
|
||||
| Indonesian (Bahasa Indonesia) (id_ID) |  |
|
||||
| Irish (Gaeilge) (ga_IE) |  |
|
||||
| Italian (Italiano) (it_IT) |  |
|
||||
| Japanese (日本語) (ja_JP) |  |
|
||||
| Korean (한국어) (ko_KR) |  |
|
||||
| Norwegian (Norsk) (no_NB) |  |
|
||||
| Polish (Polski) (pl_PL) |  |
|
||||
| Portuguese (Português) (pt_PT) |  |
|
||||
| French (Français) (fr_FR) |  |
|
||||
| German (Deutsch) (de_DE) |  |
|
||||
| Greek (Ελληνικά) (el_GR) |  |
|
||||
| Hindi (हिंदी) (hi_IN) |  |
|
||||
| Hungarian (Magyar) (hu_HU) |  |
|
||||
| Indonesian (Bahasa Indonesia) (id_ID) |  |
|
||||
| Irish (Gaeilge) (ga_IE) |  |
|
||||
| Italian (Italiano) (it_IT) |  |
|
||||
| Japanese (日本語) (ja_JP) |  |
|
||||
| Korean (한국어) (ko_KR) |  |
|
||||
| Norwegian (Norsk) (no_NB) |  |
|
||||
| Persian (فارسی) (fa_IR) |  |
|
||||
| Polish (Polski) (pl_PL) |  |
|
||||
| Portuguese (Português) (pt_PT) |  |
|
||||
| Portuguese Brazilian (Português) (pt_BR) |  |
|
||||
| Romanian (Română) (ro_RO) |  |
|
||||
| Russian (Русский) (ru_RU) |  |
|
||||
| Serbian Latin alphabet (Srpski) (sr_LATN_RS) |  |
|
||||
| Simplified Chinese (简体中文) (zh_CN) |  |
|
||||
| Slovakian (Slovensky) (sk_SK) |  |
|
||||
| Spanish (Español) (es_ES) |  |
|
||||
| Swedish (Svenska) (sv_SE) |  |
|
||||
| Thai (ไทย) (th_TH) |  |
|
||||
| Traditional Chinese (繁體中文) (zh_TW) |  |
|
||||
| Turkish (Türkçe) (tr_TR) |  |
|
||||
| Ukrainian (Українська) (uk_UA) |  |
|
||||
| Vietnamese (Tiếng Việt) (vi_VN) |  |
|
||||
| Romanian (Română) (ro_RO) |  |
|
||||
| Russian (Русский) (ru_RU) |  |
|
||||
| Serbian Latin alphabet (Srpski) (sr_LATN_RS) |  |
|
||||
| Simplified Chinese (简体中文) (zh_CN) |  |
|
||||
| Slovakian (Slovensky) (sk_SK) |  |
|
||||
| Spanish (Español) (es_ES) |  |
|
||||
| Swedish (Svenska) (sv_SE) |  |
|
||||
| Thai (ไทย) (th_TH) |  |
|
||||
| Traditional Chinese (繁體中文) (zh_TW) |  |
|
||||
| Turkish (Türkçe) (tr_TR) |  |
|
||||
| Ukrainian (Українська) (uk_UA) |  |
|
||||
| Vietnamese (Tiếng Việt) (vi_VN) |  |
|
||||
|
||||
## Contributing (Creating Issues, Translations, Fixing Bugs, etc.)
|
||||
|
||||
@@ -238,7 +241,7 @@ Stirling PDF offers a Enterprise edition of its software, This is the same great
|
||||
### Whats included
|
||||
|
||||
- Prioritised Support tickets via support@stirlingpdf.com to reach directly to Stirling-PDF team for support and 1:1 meetings where applicable (Provided they come from same email domain registered with us)
|
||||
- Prioritised Enhancements to Stirling-PDF where applicable
|
||||
- Prioritised Enhancements to Stirling-PDF where applicable
|
||||
- Base SSO support
|
||||
- Advanced SSO such as automated login handling (Coming very soon)
|
||||
- SAML SSO (Coming very soon)
|
||||
@@ -402,7 +405,7 @@ To access your account settings, go to Account Settings in the settings cog menu
|
||||
|
||||
To add new users, go to the bottom of Account Settings and hit 'Admin Settings'. Here you can add new users. The different roles mentioned within this are for rate limiting. This is a work in progress and will be expanded on more in the future.
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ The 'Fat' container contains all those found in 'Full' with security jar along w
|
||||
| Libre | | ✔️ |
|
||||
| Python | | ✔️ |
|
||||
| OpenCV | | ✔️ |
|
||||
| OCRmyPDF | | ✔️ |
|
||||
| qpdf | | ✔️ |
|
||||
|
||||
| Operation | Ultra-Lite | Full |
|
||||
| ---------------------- | ---------- | ---- |
|
||||
|
||||
227
build.gradle
227
build.gradle
@@ -1,6 +1,6 @@
|
||||
plugins {
|
||||
id "java"
|
||||
id "org.springframework.boot" version "3.3.5"
|
||||
id "org.springframework.boot" version "3.4.0"
|
||||
id "io.spring.dependency-management" version "1.1.6"
|
||||
id "org.springdoc.openapi-gradle-plugin" version "1.8.0"
|
||||
id "io.swagger.swaggerhub" version "1.3.2"
|
||||
@@ -8,21 +8,27 @@ plugins {
|
||||
id "com.diffplug.spotless" version "6.25.0"
|
||||
id "com.github.jk1.dependency-license-report" version "2.9"
|
||||
//id "nebula.lint" version "19.0.3"
|
||||
id("org.panteleyev.jpackageplugin") version "1.6.0"
|
||||
}
|
||||
|
||||
|
||||
|
||||
import com.github.jk1.license.render.*
|
||||
|
||||
ext {
|
||||
springBootVersion = "3.3.5"
|
||||
springBootVersion = "3.4.0"
|
||||
pdfboxVersion = "3.0.3"
|
||||
logbackVersion = "1.5.7"
|
||||
imageioVersion = "3.12.0"
|
||||
lombokVersion = "1.18.34"
|
||||
bouncycastleVersion = "1.78.1"
|
||||
lombokVersion = "1.18.36"
|
||||
bouncycastleVersion = "1.79"
|
||||
springSecuritySamlVersion = "6.4.2"
|
||||
openSamlVersion = "4.3.2"
|
||||
}
|
||||
|
||||
group = "stirling.software"
|
||||
version = "0.32.0"
|
||||
version = "0.36.5"
|
||||
|
||||
|
||||
java {
|
||||
// 17 is lowest but we support and recommend 21
|
||||
@@ -36,6 +42,9 @@ repositories {
|
||||
maven {
|
||||
url 'https://build.shibboleth.net/maven/releases'
|
||||
}
|
||||
maven { url "https://build.shibboleth.net/maven/releases" }
|
||||
maven { url "https://maven.pkg.github.com/jcefmaven/jcefmaven" }
|
||||
|
||||
}
|
||||
|
||||
licenseReport {
|
||||
@@ -59,6 +68,12 @@ sourceSets {
|
||||
exclude "stirling/software/SPDF/model/User.java"
|
||||
exclude "stirling/software/SPDF/repository/**"
|
||||
}
|
||||
|
||||
if (System.getenv("STIRLING_PDF_DESKTOP_UI") == "false") {
|
||||
exclude "stirling/software/SPDF/UI/impl/**"
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -69,16 +84,153 @@ openApi {
|
||||
outputFileName = "SwaggerDoc.json"
|
||||
}
|
||||
|
||||
//0.11.5 to 2024.11.5
|
||||
def getMacVersion(String version) {
|
||||
def currentYear = java.time.Year.now().getValue()
|
||||
def versionParts = version.split("\\.", 2)
|
||||
return "${currentYear}.${versionParts.length > 1 ? versionParts[1] : versionParts[0]}"
|
||||
}
|
||||
|
||||
jpackage {
|
||||
input = "build/libs"
|
||||
|
||||
appName = "Stirling-PDF"
|
||||
appVersion = project.version
|
||||
vendor = "Stirling-Software"
|
||||
appDescription = "Stirling PDF - Your Local PDF Editor"
|
||||
|
||||
mainJar = "Stirling-PDF-${project.version}.jar"
|
||||
mainClass = "org.springframework.boot.loader.launch.JarLauncher"
|
||||
|
||||
icon = "src/main/resources/static/favicon.ico"
|
||||
|
||||
|
||||
|
||||
// JVM Options
|
||||
javaOptions = [
|
||||
"-DBROWSER_OPEN=true",
|
||||
"-DSTIRLING_PDF_DESKTOP_UI=true",
|
||||
"-Djava.awt.headless=false",
|
||||
"-Dapple.awt.UIElement=true",
|
||||
"--add-opens", "java.base/java.lang=ALL-UNNAMED",
|
||||
"--add-opens", "java.desktop/java.awt.event=ALL-UNNAMED",
|
||||
"--add-opens", "java.desktop/sun.awt=ALL-UNNAMED"
|
||||
|
||||
]
|
||||
|
||||
|
||||
verbose = true
|
||||
|
||||
destination = "${projectDir}/build/jpackage"
|
||||
|
||||
// Windows-specific configuration
|
||||
windows {
|
||||
launcherAsService = false
|
||||
appVersion = project.version
|
||||
winConsole = false
|
||||
winDirChooser = true
|
||||
winMenu = true
|
||||
winShortcut = true
|
||||
winPerUserInstall = true
|
||||
winMenuGroup = "Stirling Software"
|
||||
winUpgradeUuid = "2a43ed0c-b8c2-40cf-89e1-751129b87641" // Unique identifier for updates
|
||||
winHelpUrl = "https://github.com/Stirling-Tools/Stirling-PDF"
|
||||
winUpdateUrl = "https://github.com/Stirling-Tools/Stirling-PDF/releases"
|
||||
type = "exe"
|
||||
installDir = "C:/Program Files/Stirling-PDF"
|
||||
}
|
||||
|
||||
// macOS-specific configuration
|
||||
mac {
|
||||
appVersion = getMacVersion(project.version.toString())
|
||||
icon = "src/main/resources/static/favicon.icns"
|
||||
type = "dmg"
|
||||
macPackageIdentifier = "com.stirling.software.pdf"
|
||||
macPackageName = "Stirling-PDF"
|
||||
macAppCategory = "public.app-category.productivity"
|
||||
macSign = false // Enable signing
|
||||
macAppStore = false // Not targeting App Store initially
|
||||
|
||||
//installDir = "Applications"
|
||||
|
||||
// Add license and other documentation to DMG
|
||||
/*macDmgContent = [
|
||||
"README.md",
|
||||
"LICENSE",
|
||||
"CHANGELOG.md"
|
||||
]*/
|
||||
|
||||
// Enable Mac-specific entitlements
|
||||
//macEntitlements = "entitlements.plist" // You'll need to create this file
|
||||
}
|
||||
|
||||
// Linux-specific configuration
|
||||
linux {
|
||||
appVersion = project.version
|
||||
icon = "src/main/resources/static/favicon.png"
|
||||
type = "deb" // Can also use "rpm" for Red Hat-based systems
|
||||
|
||||
// Debian package configuration
|
||||
//linuxPackageName = "stirlingpdf"
|
||||
linuxDebMaintainer = "support@stirlingpdf.com"
|
||||
linuxMenuGroup = "Office;PDF;Productivity"
|
||||
linuxAppCategory = "Office"
|
||||
linuxAppRelease = "1"
|
||||
linuxPackageDeps = true
|
||||
|
||||
installDir = "/opt/Stirling-PDF"
|
||||
|
||||
// RPM-specific settings
|
||||
//linuxRpmLicenseType = "MIT"
|
||||
}
|
||||
|
||||
// Common additional options
|
||||
//jLinkOptions = [
|
||||
// "--strip-debug",
|
||||
// "--compress=2",
|
||||
// "--no-header-files",
|
||||
// "--no-man-pages"
|
||||
//]
|
||||
|
||||
// Add any additional modules required
|
||||
/*addModules = [
|
||||
"java.base",
|
||||
"java.desktop",
|
||||
"java.logging",
|
||||
"java.sql",
|
||||
"java.xml",
|
||||
"jdk.crypto.ec"
|
||||
]*/
|
||||
|
||||
// Add copyright and license information
|
||||
copyright = "Copyright © 2024 Stirling Software"
|
||||
licenseFile = "LICENSE"
|
||||
}
|
||||
|
||||
|
||||
launch4j {
|
||||
icon = "${projectDir}/src/main/resources/static/favicon.ico"
|
||||
|
||||
outfile="Stirling-PDF.exe"
|
||||
headerType="console"
|
||||
|
||||
if(System.getenv("STIRLING_PDF_DESKTOP_UI") == 'true') {
|
||||
headerType = "gui"
|
||||
} else {
|
||||
headerType = "console"
|
||||
}
|
||||
jarTask = tasks.bootJar
|
||||
|
||||
errTitle="Encountered error, Do you have Java 21?"
|
||||
downloadUrl="https://download.oracle.com/java/21/latest/jdk-21_windows-x64_bin.exe"
|
||||
variables=["BROWSER_OPEN=true"]
|
||||
|
||||
if(System.getenv("STIRLING_PDF_DESKTOP_UI") == 'true') {
|
||||
variables=["BROWSER_OPEN=true", "STIRLING_PDF_DESKTOP_UI=true"]
|
||||
} else {
|
||||
variables=["BROWSER_OPEN=true"]
|
||||
}
|
||||
|
||||
|
||||
|
||||
jreMinVersion="17"
|
||||
|
||||
mutexName="Stirling-PDF"
|
||||
@@ -95,7 +247,7 @@ spotless {
|
||||
java {
|
||||
target project.fileTree('src/main/java')
|
||||
|
||||
googleJavaFormat("1.22.0").aosp().reorderImports(false)
|
||||
googleJavaFormat("1.25.2").aosp().reorderImports(false)
|
||||
|
||||
importOrder("java", "javax", "org", "com", "net", "io")
|
||||
toggleOffOn()
|
||||
@@ -118,10 +270,17 @@ configurations.all {
|
||||
exclude group: "org.springframework.boot", module: "spring-boot-starter-tomcat"
|
||||
}
|
||||
dependencies {
|
||||
//security updates
|
||||
implementation "org.springframework:spring-webmvc:6.1.14"
|
||||
|
||||
implementation("io.github.pixee:java-security-toolkit:1.2.0")
|
||||
if (System.getenv("STIRLING_PDF_DESKTOP_UI") != "false") {
|
||||
implementation "me.friwi:jcefmaven:127.3.1"
|
||||
implementation "org.openjfx:javafx-controls:21"
|
||||
implementation "org.openjfx:javafx-swing:21"
|
||||
}
|
||||
|
||||
//security updates
|
||||
implementation "org.springframework:spring-webmvc:6.2.1"
|
||||
|
||||
implementation("io.github.pixee:java-security-toolkit:1.2.1")
|
||||
|
||||
// implementation "org.yaml:snakeyaml:2.2"
|
||||
implementation 'com.github.Carleslc.Simple-YAML:Simple-Yaml:1.8.4'
|
||||
@@ -137,22 +296,22 @@ dependencies {
|
||||
|
||||
if (System.getenv("DOCKER_ENABLE_SECURITY") != "false") {
|
||||
implementation "org.springframework.boot:spring-boot-starter-security:$springBootVersion"
|
||||
implementation "org.thymeleaf.extras:thymeleaf-extras-springsecurity5:3.1.2.RELEASE"
|
||||
implementation "org.thymeleaf.extras:thymeleaf-extras-springsecurity5:3.1.3.RELEASE"
|
||||
implementation "org.springframework.boot:spring-boot-starter-data-jpa:$springBootVersion"
|
||||
implementation "org.springframework.boot:spring-boot-starter-oauth2-client:$springBootVersion"
|
||||
|
||||
implementation 'org.springframework.security:spring-security-saml2-service-provider:6.3.4'
|
||||
implementation 'com.unboundid.product.scim2:scim2-sdk-client:2.3.5'
|
||||
//2.2.x requires rebuild of DB file.. need migration path
|
||||
runtimeOnly "com.h2database:h2:2.1.214"
|
||||
// implementation "com.h2database:h2:2.2.224"
|
||||
constraints {
|
||||
implementation "org.opensaml:opensaml-core"
|
||||
implementation "org.opensaml:opensaml-saml-api"
|
||||
implementation "org.opensaml:opensaml-saml-impl"
|
||||
}
|
||||
implementation "org.springframework.security:spring-security-saml2-service-provider"
|
||||
implementation "org.springframework.session:spring-session-core:$springBootVersion"
|
||||
|
||||
implementation 'com.unboundid.product.scim2:scim2-sdk-client:2.3.5'
|
||||
// Don't upgrade h2database
|
||||
runtimeOnly "com.h2database:h2:2.3.232"
|
||||
constraints {
|
||||
implementation "org.opensaml:opensaml-core:$openSamlVersion"
|
||||
implementation "org.opensaml:opensaml-saml-api:$openSamlVersion"
|
||||
implementation "org.opensaml:opensaml-saml-impl:$openSamlVersion"
|
||||
}
|
||||
implementation "org.springframework.security:spring-security-saml2-service-provider:$springSecuritySamlVersion"
|
||||
// implementation 'org.springframework.security:spring-security-core:$springSecuritySamlVersion'
|
||||
implementation 'com.coveo:saml-client:5.0.0'
|
||||
|
||||
|
||||
@@ -184,7 +343,7 @@ dependencies {
|
||||
// Image metadata extractor
|
||||
implementation "com.drewnoakes:metadata-extractor:2.19.0"
|
||||
|
||||
implementation "commons-io:commons-io:2.17.0"
|
||||
implementation "commons-io:commons-io:2.18.0"
|
||||
implementation "org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0"
|
||||
//general PDF
|
||||
|
||||
@@ -201,12 +360,19 @@ dependencies {
|
||||
exclude group: "commons-logging", module: "commons-logging"
|
||||
}
|
||||
|
||||
// https://mvnrepository.com/artifact/technology.tabula/tabula
|
||||
implementation ('technology.tabula:tabula:1.0.5') {
|
||||
exclude group: "org.slf4j", module: "slf4j-simple"
|
||||
exclude group: "org.bouncycastle", module: "bcprov-jdk15on"
|
||||
exclude group: "com.google.code.gson", module: "gson"
|
||||
}
|
||||
|
||||
implementation 'org.apache.pdfbox:jbig2-imageio:3.0.4'
|
||||
|
||||
implementation "org.bouncycastle:bcprov-jdk18on:$bouncycastleVersion"
|
||||
implementation "org.bouncycastle:bcpkix-jdk18on:$bouncycastleVersion"
|
||||
implementation "org.springframework.boot:spring-boot-starter-actuator:$springBootVersion"
|
||||
implementation "io.micrometer:micrometer-core:1.13.6"
|
||||
implementation "io.micrometer:micrometer-core:1.14.2"
|
||||
implementation group: "com.google.zxing", name: "core", version: "3.5.3"
|
||||
// https://mvnrepository.com/artifact/org.commonmark/commonmark
|
||||
implementation "org.commonmark:commonmark:0.24.0"
|
||||
@@ -259,7 +425,14 @@ jar {
|
||||
tasks.named("test") {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
task printVersion {
|
||||
println project.version
|
||||
doLast {
|
||||
println project.version
|
||||
}
|
||||
}
|
||||
|
||||
task printMacVersion {
|
||||
doLast {
|
||||
println getMacVersion(project.version.toString())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,24 +48,6 @@ Feature: API Validation
|
||||
And the response status code should be 200
|
||||
|
||||
|
||||
|
||||
@ocr @negative
|
||||
Scenario: Process PDF with text and OCR with type normal
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 3 pages with random text
|
||||
And the request data includes
|
||||
| parameter | value |
|
||||
| languages | eng |
|
||||
| sidecar | false |
|
||||
| deskew | true |
|
||||
| clean | true |
|
||||
| cleanFinal | true |
|
||||
| ocrType | Normal |
|
||||
| ocrRenderType | hocr |
|
||||
| removeImagesAfter| false |
|
||||
When I send the API request to the endpoint "/api/v1/misc/ocr-pdf"
|
||||
Then the response status code should be 500
|
||||
|
||||
@ocr @positive
|
||||
Scenario: Process PDF with OCR
|
||||
Given I generate a PDF file as "fileInput"
|
||||
@@ -83,26 +65,6 @@ Feature: API Validation
|
||||
Then the response content type should be "application/pdf"
|
||||
And the response file should have size greater than 0
|
||||
And the response status code should be 200
|
||||
|
||||
@ocr @positive
|
||||
Scenario: Process PDF with OCR with sidecar
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the request data includes
|
||||
| parameter | value |
|
||||
| languages | eng |
|
||||
| sidecar | true |
|
||||
| deskew | true |
|
||||
| clean | true |
|
||||
| cleanFinal | true |
|
||||
| ocrType | Force |
|
||||
| ocrRenderType | hocr |
|
||||
| removeImagesAfter| false |
|
||||
When I send the API request to the endpoint "/api/v1/misc/ocr-pdf"
|
||||
Then the response content type should be "application/octet-stream"
|
||||
And the response file should have extension ".zip"
|
||||
And the response ZIP should contain 2 files
|
||||
And the response file should have size greater than 0
|
||||
And the response status code should be 200
|
||||
|
||||
|
||||
@libre @positive
|
||||
@@ -145,7 +107,7 @@ Feature: API Validation
|
||||
And the response file should have extension ".pdf"
|
||||
And the response file should have size greater than 100
|
||||
|
||||
@compress @ghostscript @positive
|
||||
@compress @qpdf @positive
|
||||
Scenario: Compress
|
||||
Given I use an example file at "exampleFiles/ghost3.pdf" as parameter "fileInput"
|
||||
And the request data includes
|
||||
@@ -156,7 +118,7 @@ Feature: API Validation
|
||||
And the response file should have extension ".pdf"
|
||||
And the response file should have size greater than 100
|
||||
|
||||
@compress @ghostscript @positive
|
||||
@compress @qpdf @positive
|
||||
Scenario: Compress
|
||||
Given I use an example file at "exampleFiles/ghost2.pdf" as parameter "fileInput"
|
||||
And the request data includes
|
||||
@@ -169,7 +131,7 @@ Feature: API Validation
|
||||
And the response file should have size greater than 100
|
||||
|
||||
|
||||
@compress @ghostscript @positive
|
||||
@compress @qpdf @positive
|
||||
Scenario: Compress
|
||||
Given I use an example file at "exampleFiles/ghost1.pdf" as parameter "fileInput"
|
||||
And the request data includes
|
||||
|
||||
@@ -15,6 +15,10 @@ import shutil
|
||||
import re
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
API_HEADERS = {
|
||||
'X-API-KEY': '123456789'
|
||||
}
|
||||
|
||||
#########
|
||||
# GIVEN #
|
||||
#########
|
||||
@@ -227,7 +231,7 @@ def save_generated_pdf(context, filename):
|
||||
def step_send_get_request(context, endpoint):
|
||||
base_url = "http://localhost:8080"
|
||||
full_url = f"{base_url}{endpoint}"
|
||||
response = requests.get(full_url)
|
||||
response = requests.get(full_url, headers=API_HEADERS)
|
||||
context.response = response
|
||||
|
||||
@when('I send a GET request to "{endpoint}" with parameters')
|
||||
@@ -235,7 +239,7 @@ def step_send_get_request_with_params(context, endpoint):
|
||||
base_url = "http://localhost:8080"
|
||||
params = {row['parameter']: row['value'] for row in context.table}
|
||||
full_url = f"{base_url}{endpoint}"
|
||||
response = requests.get(full_url, params=params)
|
||||
response = requests.get(full_url, params=params, headers=API_HEADERS)
|
||||
context.response = response
|
||||
|
||||
@when('I send the API request to the endpoint "{endpoint}"')
|
||||
@@ -256,7 +260,7 @@ def step_send_api_request(context, endpoint):
|
||||
print(f"form_data {file.name} with {mime_type}")
|
||||
form_data.append((key, (file.name, file, mime_type)))
|
||||
|
||||
response = requests.post(url, files=form_data)
|
||||
response = requests.post(url, files=form_data, headers=API_HEADERS)
|
||||
context.response = response
|
||||
|
||||
########
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
services:
|
||||
stirling-pdf:
|
||||
container_name: Stirling-PDF-Security-Fat
|
||||
image: frooodle/s-pdf:latest-fat
|
||||
image: stirlingtools/stirling-pdf:latest-fat
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
services:
|
||||
stirling-pdf:
|
||||
container_name: Stirling-PDF-Security
|
||||
image: frooodle/s-pdf:latest
|
||||
image: stirlingtools/stirling-pdf:latest
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
services:
|
||||
stirling-pdf:
|
||||
container_name: Stirling-PDF-Security
|
||||
image: frooodle/s-pdf:latest
|
||||
image: stirlingtools/stirling-pdf:latest
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
services:
|
||||
stirling-pdf:
|
||||
container_name: Stirling-PDF-Ultra-Lite-Security
|
||||
image: frooodle/s-pdf:latest-ultra-lite
|
||||
image: stirlingtools/stirling-pdf:latest-ultra-lite
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
services:
|
||||
stirling-pdf:
|
||||
container_name: Stirling-PDF-Ultra-Lite
|
||||
image: frooodle/s-pdf:latest-ultra-lite
|
||||
image: stirlingtools/stirling-pdf:latest-ultra-lite
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
services:
|
||||
stirling-pdf:
|
||||
container_name: Stirling-PDF
|
||||
image: frooodle/s-pdf:latest
|
||||
image: stirlingtools/stirling-pdf:latest
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
|
||||
34
exampleYmlFiles/test_cicd.yml
Normal file
34
exampleYmlFiles/test_cicd.yml
Normal file
@@ -0,0 +1,34 @@
|
||||
services:
|
||||
stirling-pdf:
|
||||
container_name: Stirling-PDF-Security-Fat
|
||||
image: stirlingtools/stirling-pdf:latest-fat
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 4G
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -f -H 'X-API-KEY: 123456789' http://localhost:8080/api/v1/info/status | grep -q 'UP'"]
|
||||
interval: 5s
|
||||
timeout: 10s
|
||||
retries: 16
|
||||
ports:
|
||||
- 8080:8080
|
||||
volumes:
|
||||
- /stirling/latest/data:/usr/share/tessdata:rw
|
||||
- /stirling/latest/config:/configs:rw
|
||||
- /stirling/latest/logs:/logs:rw
|
||||
environment:
|
||||
DOCKER_ENABLE_SECURITY: "true"
|
||||
SECURITY_ENABLELOGIN: "true"
|
||||
PUID: 1002
|
||||
PGID: 1002
|
||||
UMASK: "022"
|
||||
SYSTEM_DEFAULTLOCALE: en-US
|
||||
UI_APPNAME: Stirling-PDF
|
||||
UI_HOMEDESCRIPTION: Demo site for Stirling-PDF Latest-fat with Security
|
||||
UI_APPNAMENAVBAR: Stirling-PDF Latest-fat
|
||||
SYSTEM_MAXFILESIZE: "100"
|
||||
METRICS_ENABLED: "true"
|
||||
SYSTEM_GOOGLEVISIBILITY: "true"
|
||||
SECURITY_CUSTOMGLOBALAPIKEY: "123456789"
|
||||
restart: on-failure:5
|
||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,5 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
@@ -3,6 +3,11 @@ ignore = [
|
||||
'language.direction',
|
||||
]
|
||||
|
||||
[az_AZ]
|
||||
ignore = [
|
||||
'language.direction',
|
||||
]
|
||||
|
||||
[bg_BG]
|
||||
ignore = [
|
||||
'language.direction',
|
||||
@@ -32,21 +37,25 @@ ignore = [
|
||||
ignore = [
|
||||
'AddStampRequest.alphabet',
|
||||
'AddStampRequest.position',
|
||||
'home.pipeline.title'
|
||||
'PDFToBook.selectText.1',
|
||||
'PDFToText.tags',
|
||||
'addPageNumbers.selectText.3',
|
||||
'alphabet',
|
||||
'certSign.name',
|
||||
'fileChooser.dragAndDrop',
|
||||
'home.pipeline.title',
|
||||
'language.direction',
|
||||
'legal.impressum',
|
||||
'licenses.version',
|
||||
'pipeline.title',
|
||||
'pipelineOptions.pipelineHeader',
|
||||
'pro',
|
||||
'sponsor',
|
||||
'text',
|
||||
'validateSignature.cert.bits',
|
||||
'validateSignature.cert.version',
|
||||
'validateSignature.status',
|
||||
'watermark.type.1',
|
||||
'certSign.name',
|
||||
]
|
||||
|
||||
[el_GR]
|
||||
@@ -57,7 +66,6 @@ ignore = [
|
||||
[es_ES]
|
||||
ignore = [
|
||||
'adminUserSettings.roles',
|
||||
'color',
|
||||
'error',
|
||||
'language.direction',
|
||||
'no',
|
||||
@@ -69,6 +77,11 @@ ignore = [
|
||||
'language.direction',
|
||||
]
|
||||
|
||||
[fa_IR]
|
||||
ignore = [
|
||||
'language.direction',
|
||||
]
|
||||
|
||||
[fr_FR]
|
||||
ignore = [
|
||||
'AddStampRequest.alphabet',
|
||||
|
||||
@@ -16,10 +16,10 @@ fi
|
||||
# Check if TESSERACT_LANGS environment variable is set and is not empty
|
||||
if [[ -n "$TESSERACT_LANGS" ]]; then
|
||||
# Convert comma-separated values to a space-separated list
|
||||
LANGS=$(echo $TESSERACT_LANGS | tr ',' ' ')
|
||||
SPACE_SEPARATED_LANGS=$(echo $TESSERACT_LANGS | tr ',' ' ')
|
||||
pattern='^[a-zA-Z]{2,4}(_[a-zA-Z]{2,4})?$'
|
||||
# Install each language pack
|
||||
for LANG in $LANGS; do
|
||||
for LANG in $SPACE_SEPARATED_LANGS; do
|
||||
if [[ $LANG =~ $pattern ]]; then
|
||||
apk add --no-cache "tesseract-ocr-data-$LANG"
|
||||
else
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#!/bin/bash
|
||||
|
||||
translation_key="pdfToPDFA.credit"
|
||||
old_value="OCRmyPDF"
|
||||
new_value="ghostscript"
|
||||
old_value="qpdf"
|
||||
new_value="liibreoffice"
|
||||
|
||||
for file in ../src/main/resources/messages_*.properties; do
|
||||
sed -i "/^$translation_key=/s/$old_value/$new_value/" "$file"
|
||||
|
||||
@@ -3,13 +3,14 @@ package stirling.software.SPDF.EE;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.SPDF.model.ApplicationProperties;
|
||||
|
||||
@Configuration
|
||||
@Lazy
|
||||
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||||
@Slf4j
|
||||
public class EEAppConfig {
|
||||
|
||||
|
||||
@@ -25,9 +25,10 @@ public class LicenseKeyChecker {
|
||||
KeygenLicenseVerifier licenseService, ApplicationProperties applicationProperties) {
|
||||
this.licenseService = licenseService;
|
||||
this.applicationProperties = applicationProperties;
|
||||
this.checkLicense();
|
||||
}
|
||||
|
||||
@Scheduled(fixedRate = 604800000, initialDelay = 1000) // 7 days in milliseconds
|
||||
@Scheduled(initialDelay = 604800000, fixedRate = 604800000) // 7 days in milliseconds
|
||||
public void checkLicensePeriodically() {
|
||||
checkLicense();
|
||||
}
|
||||
|
||||
@@ -6,9 +6,6 @@ import java.net.Socket;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import io.github.pixee.security.SystemCommand;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -16,7 +13,6 @@ import lombok.extern.slf4j.Slf4j;
|
||||
@Slf4j
|
||||
public class LibreOfficeListener {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(LibreOfficeListener.class);
|
||||
private static final long ACTIVITY_TIMEOUT = 20L * 60 * 1000; // 20 minutes
|
||||
|
||||
private static final LibreOfficeListener INSTANCE = new LibreOfficeListener();
|
||||
@@ -87,7 +83,7 @@ public class LibreOfficeListener {
|
||||
Thread.sleep(1000);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
logger.error("exception", e);
|
||||
log.error("exception", e);
|
||||
} // Check every 1 second
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package stirling.software.SPDF;
|
||||
|
||||
import java.awt.*;
|
||||
import java.io.IOException;
|
||||
import java.net.ServerSocket;
|
||||
import java.nio.file.Files;
|
||||
@@ -8,9 +9,10 @@ import java.nio.file.Paths;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
|
||||
import javax.swing.*;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
@@ -21,15 +23,17 @@ import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
import io.github.pixee.security.SystemCommand;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.SPDF.UI.WebBrowser;
|
||||
import stirling.software.SPDF.config.ConfigInitializer;
|
||||
import stirling.software.SPDF.model.ApplicationProperties;
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableScheduling
|
||||
@Slf4j
|
||||
public class SPdfApplication {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(SPdfApplication.class);
|
||||
|
||||
@Autowired private Environment env;
|
||||
@Autowired ApplicationProperties applicationProperties;
|
||||
|
||||
@@ -67,36 +71,19 @@ public class SPdfApplication {
|
||||
}
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
baseUrlStatic = this.baseUrl;
|
||||
// Check if the BROWSER_OPEN environment variable is set to true
|
||||
String browserOpenEnv = env.getProperty("BROWSER_OPEN");
|
||||
boolean browserOpen = browserOpenEnv != null && "true".equalsIgnoreCase(browserOpenEnv);
|
||||
if (browserOpen) {
|
||||
try {
|
||||
String url = baseUrl + ":" + getStaticPort();
|
||||
|
||||
String os = System.getProperty("os.name").toLowerCase();
|
||||
Runtime rt = Runtime.getRuntime();
|
||||
if (os.contains("win")) {
|
||||
// For Windows
|
||||
SystemCommand.runCommand(rt, "rundll32 url.dll,FileProtocolHandler " + url);
|
||||
} else if (os.contains("mac")) {
|
||||
SystemCommand.runCommand(rt, "open " + url);
|
||||
} else if (os.contains("nix") || os.contains("nux")) {
|
||||
SystemCommand.runCommand(rt, "xdg-open " + url);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Error opening browser: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
logger.info("Running configs {}", applicationProperties.toString());
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws IOException, InterruptedException {
|
||||
|
||||
SpringApplication app = new SpringApplication(SPdfApplication.class);
|
||||
|
||||
Properties props = new Properties();
|
||||
|
||||
if (Boolean.parseBoolean(System.getProperty("STIRLING_PDF_DESKTOP_UI", "false"))) {
|
||||
System.setProperty("java.awt.headless", "false");
|
||||
app.setHeadless(false);
|
||||
props.put("java.awt.headless", "false");
|
||||
props.put("spring.main.web-application-type", "servlet");
|
||||
}
|
||||
|
||||
app.setAdditionalProfiles("default");
|
||||
app.addInitializers(new ConfigInitializer());
|
||||
Map<String, String> propertyFiles = new HashMap<>();
|
||||
@@ -105,7 +92,7 @@ public class SPdfApplication {
|
||||
if (Files.exists(Paths.get("configs/settings.yml"))) {
|
||||
propertyFiles.put("spring.config.additional-location", "file:configs/settings.yml");
|
||||
} else {
|
||||
logger.warn("External configuration file 'configs/settings.yml' does not exist.");
|
||||
log.warn("External configuration file 'configs/settings.yml' does not exist.");
|
||||
}
|
||||
|
||||
if (Files.exists(Paths.get("configs/custom_settings.yml"))) {
|
||||
@@ -118,16 +105,22 @@ public class SPdfApplication {
|
||||
"spring.config.additional-location",
|
||||
existingLocation + "file:configs/custom_settings.yml");
|
||||
} else {
|
||||
logger.warn("Custom configuration file 'configs/custom_settings.yml' does not exist.");
|
||||
log.warn("Custom configuration file 'configs/custom_settings.yml' does not exist.");
|
||||
}
|
||||
Properties finalProps = new Properties();
|
||||
|
||||
if (!propertyFiles.isEmpty()) {
|
||||
app.setDefaultProperties(
|
||||
finalProps.putAll(
|
||||
Collections.singletonMap(
|
||||
"spring.config.additional-location",
|
||||
propertyFiles.get("spring.config.additional-location")));
|
||||
}
|
||||
|
||||
if (!props.isEmpty()) {
|
||||
finalProps.putAll(props);
|
||||
}
|
||||
app.setDefaultProperties(finalProps);
|
||||
|
||||
app.run(args);
|
||||
|
||||
// Ensure directories are created
|
||||
@@ -135,16 +128,56 @@ public class SPdfApplication {
|
||||
Files.createDirectories(Path.of("customFiles/static/"));
|
||||
Files.createDirectories(Path.of("customFiles/templates/"));
|
||||
} catch (Exception e) {
|
||||
logger.error("Error creating directories: {}", e.getMessage());
|
||||
log.error("Error creating directories: {}", e.getMessage());
|
||||
}
|
||||
|
||||
printStartupLogs();
|
||||
}
|
||||
|
||||
private static void printStartupLogs() {
|
||||
logger.info("Stirling-PDF Started.");
|
||||
log.info("Stirling-PDF Started.");
|
||||
String url = baseUrlStatic + ":" + getStaticPort();
|
||||
logger.info("Navigate to {}", url);
|
||||
log.info("Navigate to {}", url);
|
||||
}
|
||||
|
||||
@Autowired(required = false)
|
||||
private WebBrowser webBrowser;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
baseUrlStatic = this.baseUrl;
|
||||
String url = baseUrl + ":" + getStaticPort();
|
||||
if (webBrowser != null
|
||||
&& Boolean.parseBoolean(System.getProperty("STIRLING_PDF_DESKTOP_UI", "false"))) {
|
||||
webBrowser.initWebUI(url);
|
||||
} else {
|
||||
String browserOpenEnv = env.getProperty("BROWSER_OPEN");
|
||||
boolean browserOpen = browserOpenEnv != null && "true".equalsIgnoreCase(browserOpenEnv);
|
||||
if (browserOpen) {
|
||||
try {
|
||||
String os = System.getProperty("os.name").toLowerCase();
|
||||
Runtime rt = Runtime.getRuntime();
|
||||
if (os.contains("win")) {
|
||||
// For Windows
|
||||
SystemCommand.runCommand(rt, "rundll32 url.dll,FileProtocolHandler " + url);
|
||||
} else if (os.contains("mac")) {
|
||||
SystemCommand.runCommand(rt, "open " + url);
|
||||
} else if (os.contains("nix") || os.contains("nux")) {
|
||||
SystemCommand.runCommand(rt, "xdg-open " + url);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Error opening browser: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
log.info("Running configs {}", applicationProperties.toString());
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void cleanup() {
|
||||
if (webBrowser != null) {
|
||||
webBrowser.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
public static String getStaticBaseUrl() {
|
||||
|
||||
7
src/main/java/stirling/software/SPDF/UI/WebBrowser.java
Normal file
7
src/main/java/stirling/software/SPDF/UI/WebBrowser.java
Normal file
@@ -0,0 +1,7 @@
|
||||
package stirling.software.SPDF.UI;
|
||||
|
||||
public interface WebBrowser {
|
||||
void initWebUI(String url);
|
||||
|
||||
void cleanup();
|
||||
}
|
||||
354
src/main/java/stirling/software/SPDF/UI/impl/DesktopBrowser.java
Normal file
354
src/main/java/stirling/software/SPDF/UI/impl/DesktopBrowser.java
Normal file
@@ -0,0 +1,354 @@
|
||||
package stirling.software.SPDF.UI.impl;
|
||||
|
||||
import java.awt.AWTException;
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.Frame;
|
||||
import java.awt.Image;
|
||||
import java.awt.MenuItem;
|
||||
import java.awt.PopupMenu;
|
||||
import java.awt.SystemTray;
|
||||
import java.awt.TrayIcon;
|
||||
import java.awt.event.WindowEvent;
|
||||
import java.awt.event.WindowStateListener;
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.swing.JFrame;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.SwingUtilities;
|
||||
import javax.swing.Timer;
|
||||
|
||||
import org.cef.CefApp;
|
||||
import org.cef.CefClient;
|
||||
import org.cef.CefSettings;
|
||||
import org.cef.browser.CefBrowser;
|
||||
import org.cef.callback.CefBeforeDownloadCallback;
|
||||
import org.cef.callback.CefDownloadItem;
|
||||
import org.cef.callback.CefDownloadItemCallback;
|
||||
import org.cef.handler.CefDownloadHandlerAdapter;
|
||||
import org.cef.handler.CefLoadHandlerAdapter;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import me.friwi.jcefmaven.CefAppBuilder;
|
||||
import me.friwi.jcefmaven.EnumProgress;
|
||||
import me.friwi.jcefmaven.MavenCefAppHandlerAdapter;
|
||||
import me.friwi.jcefmaven.impl.progress.ConsoleProgressHandler;
|
||||
import stirling.software.SPDF.UI.WebBrowser;
|
||||
|
||||
@Component
|
||||
@Slf4j
|
||||
@ConditionalOnProperty(
|
||||
name = "STIRLING_PDF_DESKTOP_UI",
|
||||
havingValue = "true",
|
||||
matchIfMissing = false)
|
||||
public class DesktopBrowser implements WebBrowser {
|
||||
private static CefApp cefApp;
|
||||
private static CefClient client;
|
||||
private static CefBrowser browser;
|
||||
private static JFrame frame;
|
||||
private static LoadingWindow loadingWindow;
|
||||
private static volatile boolean browserInitialized = false;
|
||||
private static TrayIcon trayIcon;
|
||||
private static SystemTray systemTray;
|
||||
|
||||
public DesktopBrowser() {
|
||||
SwingUtilities.invokeLater(
|
||||
() -> {
|
||||
loadingWindow = new LoadingWindow(null, "Initializing...");
|
||||
loadingWindow.setVisible(true);
|
||||
});
|
||||
}
|
||||
|
||||
public void initWebUI(String url) {
|
||||
CompletableFuture.runAsync(
|
||||
() -> {
|
||||
try {
|
||||
CefAppBuilder builder = new CefAppBuilder();
|
||||
configureCefSettings(builder);
|
||||
builder.setProgressHandler(createProgressHandler());
|
||||
|
||||
// Build and initialize CEF
|
||||
cefApp = builder.build();
|
||||
client = cefApp.createClient();
|
||||
|
||||
// Set up download handler
|
||||
setupDownloadHandler();
|
||||
|
||||
// Create browser and frame on EDT
|
||||
SwingUtilities.invokeAndWait(
|
||||
() -> {
|
||||
browser = client.createBrowser(url, false, false);
|
||||
setupMainFrame();
|
||||
setupLoadHandler();
|
||||
|
||||
// Show the frame immediately but transparent
|
||||
frame.setVisible(true);
|
||||
});
|
||||
} catch (Exception e) {
|
||||
log.error("Error initializing JCEF browser: ", e);
|
||||
cleanup();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void configureCefSettings(CefAppBuilder builder) {
|
||||
CefSettings settings = builder.getCefSettings();
|
||||
settings.cache_path = new File("jcef-bundle").getAbsolutePath();
|
||||
settings.root_cache_path = new File("jcef-bundle").getAbsolutePath();
|
||||
settings.persist_session_cookies = true;
|
||||
settings.windowless_rendering_enabled = false;
|
||||
settings.log_severity = CefSettings.LogSeverity.LOGSEVERITY_INFO;
|
||||
|
||||
builder.setAppHandler(
|
||||
new MavenCefAppHandlerAdapter() {
|
||||
@Override
|
||||
public void stateHasChanged(org.cef.CefApp.CefAppState state) {
|
||||
log.info("CEF state changed: " + state);
|
||||
if (state == CefApp.CefAppState.TERMINATED) {
|
||||
System.exit(0);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void setupDownloadHandler() {
|
||||
client.addDownloadHandler(
|
||||
new CefDownloadHandlerAdapter() {
|
||||
@Override
|
||||
public boolean onBeforeDownload(
|
||||
CefBrowser browser,
|
||||
CefDownloadItem downloadItem,
|
||||
String suggestedName,
|
||||
CefBeforeDownloadCallback callback) {
|
||||
callback.Continue("", true);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDownloadUpdated(
|
||||
CefBrowser browser,
|
||||
CefDownloadItem downloadItem,
|
||||
CefDownloadItemCallback callback) {
|
||||
if (downloadItem.isComplete()) {
|
||||
log.info("Download completed: " + downloadItem.getFullPath());
|
||||
} else if (downloadItem.isCanceled()) {
|
||||
log.info("Download canceled: " + downloadItem.getFullPath());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private ConsoleProgressHandler createProgressHandler() {
|
||||
return new ConsoleProgressHandler() {
|
||||
@Override
|
||||
public void handleProgress(EnumProgress state, float percent) {
|
||||
Objects.requireNonNull(state, "state cannot be null");
|
||||
SwingUtilities.invokeLater(
|
||||
() -> {
|
||||
if (loadingWindow != null) {
|
||||
switch (state) {
|
||||
case LOCATING:
|
||||
loadingWindow.setStatus("Locating Files...");
|
||||
loadingWindow.setProgress(0);
|
||||
break;
|
||||
case DOWNLOADING:
|
||||
if (percent >= 0) {
|
||||
loadingWindow.setStatus(
|
||||
String.format(
|
||||
"Downloading additional files: %.0f%%",
|
||||
percent));
|
||||
loadingWindow.setProgress((int) percent);
|
||||
}
|
||||
break;
|
||||
case EXTRACTING:
|
||||
loadingWindow.setStatus("Extracting files...");
|
||||
loadingWindow.setProgress(60);
|
||||
break;
|
||||
case INITIALIZING:
|
||||
loadingWindow.setStatus("Initializing UI...");
|
||||
loadingWindow.setProgress(80);
|
||||
break;
|
||||
case INITIALIZED:
|
||||
loadingWindow.setStatus("Finalising startup...");
|
||||
loadingWindow.setProgress(90);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void setupMainFrame() {
|
||||
frame = new JFrame("Stirling-PDF");
|
||||
frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
|
||||
frame.setUndecorated(true);
|
||||
frame.setOpacity(0.0f);
|
||||
|
||||
JPanel contentPane = new JPanel(new BorderLayout());
|
||||
contentPane.setDoubleBuffered(true);
|
||||
contentPane.add(browser.getUIComponent(), BorderLayout.CENTER);
|
||||
frame.setContentPane(contentPane);
|
||||
|
||||
frame.addWindowListener(
|
||||
new java.awt.event.WindowAdapter() {
|
||||
@Override
|
||||
public void windowClosing(java.awt.event.WindowEvent windowEvent) {
|
||||
cleanup();
|
||||
System.exit(0);
|
||||
}
|
||||
});
|
||||
|
||||
frame.setSize(1280, 768);
|
||||
frame.setLocationRelativeTo(null);
|
||||
|
||||
loadIcon();
|
||||
}
|
||||
|
||||
private void setupLoadHandler() {
|
||||
client.addLoadHandler(
|
||||
new CefLoadHandlerAdapter() {
|
||||
@Override
|
||||
public void onLoadingStateChange(
|
||||
CefBrowser browser,
|
||||
boolean isLoading,
|
||||
boolean canGoBack,
|
||||
boolean canGoForward) {
|
||||
if (!isLoading && !browserInitialized) {
|
||||
browserInitialized = true;
|
||||
SwingUtilities.invokeLater(
|
||||
() -> {
|
||||
if (loadingWindow != null) {
|
||||
Timer timer =
|
||||
new Timer(
|
||||
500,
|
||||
e -> {
|
||||
loadingWindow.dispose();
|
||||
loadingWindow = null;
|
||||
|
||||
frame.dispose();
|
||||
frame.setOpacity(1.0f);
|
||||
frame.setUndecorated(false);
|
||||
frame.pack();
|
||||
frame.setSize(1280, 800);
|
||||
frame.setLocationRelativeTo(null);
|
||||
frame.setVisible(true);
|
||||
frame.requestFocus();
|
||||
frame.toFront();
|
||||
browser.getUIComponent()
|
||||
.requestFocus();
|
||||
});
|
||||
timer.setRepeats(false);
|
||||
timer.start();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void setupTrayIcon(Image icon) {
|
||||
if (!SystemTray.isSupported()) {
|
||||
log.warn("System tray is not supported");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
systemTray = SystemTray.getSystemTray();
|
||||
|
||||
// Create popup menu
|
||||
PopupMenu popup = new PopupMenu();
|
||||
|
||||
// Create menu items
|
||||
MenuItem showItem = new MenuItem("Show");
|
||||
showItem.addActionListener(
|
||||
e -> {
|
||||
frame.setVisible(true);
|
||||
frame.setState(Frame.NORMAL);
|
||||
});
|
||||
|
||||
MenuItem exitItem = new MenuItem("Exit");
|
||||
exitItem.addActionListener(
|
||||
e -> {
|
||||
cleanup();
|
||||
System.exit(0);
|
||||
});
|
||||
|
||||
// Add menu items to popup menu
|
||||
popup.add(showItem);
|
||||
popup.addSeparator();
|
||||
popup.add(exitItem);
|
||||
|
||||
// Create tray icon
|
||||
trayIcon = new TrayIcon(icon, "Stirling-PDF", popup);
|
||||
trayIcon.setImageAutoSize(true);
|
||||
|
||||
// Add double-click behavior
|
||||
trayIcon.addActionListener(
|
||||
e -> {
|
||||
frame.setVisible(true);
|
||||
frame.setState(Frame.NORMAL);
|
||||
});
|
||||
|
||||
// Add tray icon to system tray
|
||||
systemTray.add(trayIcon);
|
||||
|
||||
// Modify frame behavior to minimize to tray
|
||||
frame.addWindowStateListener(
|
||||
new WindowStateListener() {
|
||||
public void windowStateChanged(WindowEvent e) {
|
||||
if (e.getNewState() == Frame.ICONIFIED) {
|
||||
frame.setVisible(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
} catch (AWTException e) {
|
||||
log.error("Error setting up system tray icon", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void loadIcon() {
|
||||
try {
|
||||
Image icon = null;
|
||||
String[] iconPaths = {"/static/favicon.ico"};
|
||||
|
||||
for (String path : iconPaths) {
|
||||
if (icon != null) break;
|
||||
try {
|
||||
try (InputStream is = getClass().getResourceAsStream(path)) {
|
||||
if (is != null) {
|
||||
icon = ImageIO.read(is);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.debug("Could not load icon from " + path, e);
|
||||
}
|
||||
}
|
||||
|
||||
if (icon != null) {
|
||||
frame.setIconImage(icon);
|
||||
setupTrayIcon(icon);
|
||||
} else {
|
||||
log.warn("Could not load icon from any source");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Error loading icon", e);
|
||||
}
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void cleanup() {
|
||||
if (browser != null) browser.close(true);
|
||||
if (client != null) client.dispose();
|
||||
if (cefApp != null) cefApp.dispose();
|
||||
if (loadingWindow != null) loadingWindow.dispose();
|
||||
}
|
||||
}
|
||||
114
src/main/java/stirling/software/SPDF/UI/impl/LoadingWindow.java
Normal file
114
src/main/java/stirling/software/SPDF/UI/impl/LoadingWindow.java
Normal file
@@ -0,0 +1,114 @@
|
||||
package stirling.software.SPDF.UI.impl;
|
||||
|
||||
import java.awt.*;
|
||||
import java.io.InputStream;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.swing.*;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
public class LoadingWindow extends JDialog {
|
||||
private final JProgressBar progressBar;
|
||||
private final JLabel statusLabel;
|
||||
private final JPanel mainPanel;
|
||||
private final JLabel brandLabel;
|
||||
|
||||
public LoadingWindow(Frame parent, String initialUrl) {
|
||||
super(parent, "Initializing Stirling-PDF", true);
|
||||
|
||||
// Initialize components
|
||||
mainPanel = new JPanel();
|
||||
mainPanel.setBackground(Color.WHITE);
|
||||
mainPanel.setBorder(BorderFactory.createEmptyBorder(20, 30, 20, 30));
|
||||
mainPanel.setLayout(new GridBagLayout());
|
||||
GridBagConstraints gbc = new GridBagConstraints();
|
||||
|
||||
// Configure GridBagConstraints
|
||||
gbc.gridwidth = GridBagConstraints.REMAINDER;
|
||||
gbc.fill = GridBagConstraints.HORIZONTAL;
|
||||
gbc.insets = new Insets(5, 5, 5, 5);
|
||||
gbc.weightx = 1.0; // Add horizontal weight
|
||||
gbc.weighty = 0.0; // Add vertical weight
|
||||
|
||||
// Add icon
|
||||
try {
|
||||
try (InputStream is = getClass().getResourceAsStream("/static/favicon.ico")) {
|
||||
if (is != null) {
|
||||
Image img = ImageIO.read(is);
|
||||
if (img != null) {
|
||||
Image scaledImg = img.getScaledInstance(48, 48, Image.SCALE_SMOOTH);
|
||||
JLabel iconLabel = new JLabel(new ImageIcon(scaledImg));
|
||||
iconLabel.setHorizontalAlignment(SwingConstants.CENTER);
|
||||
gbc.gridy = 0;
|
||||
mainPanel.add(iconLabel, gbc);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to load icon", e);
|
||||
}
|
||||
// URL Label with explicit size
|
||||
brandLabel = new JLabel(initialUrl);
|
||||
brandLabel.setHorizontalAlignment(SwingConstants.CENTER);
|
||||
brandLabel.setPreferredSize(new Dimension(300, 25));
|
||||
brandLabel.setText("Stirling-PDF");
|
||||
gbc.gridy = 1;
|
||||
mainPanel.add(brandLabel, gbc);
|
||||
|
||||
// Status label with explicit size
|
||||
statusLabel = new JLabel("Initializing...");
|
||||
statusLabel.setHorizontalAlignment(SwingConstants.CENTER);
|
||||
statusLabel.setPreferredSize(new Dimension(300, 25));
|
||||
gbc.gridy = 2;
|
||||
mainPanel.add(statusLabel, gbc);
|
||||
// Progress bar with explicit size
|
||||
progressBar = new JProgressBar(0, 100);
|
||||
progressBar.setStringPainted(true);
|
||||
progressBar.setPreferredSize(new Dimension(300, 25));
|
||||
gbc.gridy = 3;
|
||||
mainPanel.add(progressBar, gbc);
|
||||
|
||||
// Set dialog properties
|
||||
setContentPane(mainPanel);
|
||||
setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE);
|
||||
setResizable(false);
|
||||
setUndecorated(false);
|
||||
|
||||
// Set size and position
|
||||
setSize(400, 200);
|
||||
setLocationRelativeTo(parent);
|
||||
setAlwaysOnTop(true);
|
||||
setProgress(0);
|
||||
setStatus("Starting...");
|
||||
}
|
||||
|
||||
public void setProgress(final int progress) {
|
||||
SwingUtilities.invokeLater(
|
||||
() -> {
|
||||
try {
|
||||
progressBar.setValue(Math.min(Math.max(progress, 0), 100));
|
||||
progressBar.setString(progress + "%");
|
||||
mainPanel.revalidate();
|
||||
mainPanel.repaint();
|
||||
} catch (Exception e) {
|
||||
log.error("Error updating progress", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void setStatus(final String status) {
|
||||
log.info(status);
|
||||
SwingUtilities.invokeLater(
|
||||
() -> {
|
||||
try {
|
||||
statusLabel.setText(status != null ? status : "");
|
||||
mainPanel.revalidate();
|
||||
mainPanel.repaint();
|
||||
} catch (Exception e) {
|
||||
log.error("Error updating status", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,6 @@ import java.nio.file.Paths;
|
||||
import java.util.Properties;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
@@ -21,14 +19,14 @@ import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.thymeleaf.spring6.SpringTemplateEngine;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.SPDF.model.ApplicationProperties;
|
||||
|
||||
@Configuration
|
||||
@Lazy
|
||||
@Slf4j
|
||||
public class AppConfig {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(AppConfig.class);
|
||||
|
||||
@Autowired ApplicationProperties applicationProperties;
|
||||
|
||||
@Bean
|
||||
@@ -61,7 +59,7 @@ public class AppConfig {
|
||||
props.load(resource.getInputStream());
|
||||
return props.getProperty("version");
|
||||
} catch (IOException e) {
|
||||
logger.error("exception", e);
|
||||
log.error("exception", e);
|
||||
}
|
||||
return "0.0.0";
|
||||
}
|
||||
@@ -101,6 +99,27 @@ public class AppConfig {
|
||||
return Files.exists(Paths.get("/.dockerenv"));
|
||||
}
|
||||
|
||||
@Bean(name = "configDirMounted")
|
||||
public boolean isRunningInDockerWithConfig() {
|
||||
Path dockerEnv = Paths.get("/.dockerenv");
|
||||
// default to true if not docker
|
||||
if (!Files.exists(dockerEnv)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
Path mountInfo = Paths.get("/proc/1/mountinfo");
|
||||
// this should always exist, if not some unknown usecase
|
||||
if (!Files.exists(mountInfo)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
return Files.lines(mountInfo).anyMatch(line -> line.contains(" /configs "));
|
||||
} catch (IOException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Bean(name = "bookAndHtmlFormatsInstalled")
|
||||
public boolean bookAndHtmlFormatsInstalled() {
|
||||
String installOps = System.getProperty("INSTALL_BOOK_AND_ADVANCED_HTML_OPS");
|
||||
|
||||
@@ -16,16 +16,15 @@ import org.simpleyaml.configuration.comments.CommentType;
|
||||
import org.simpleyaml.configuration.file.YamlFile;
|
||||
import org.simpleyaml.configuration.implementation.SimpleYamlImplementation;
|
||||
import org.simpleyaml.configuration.implementation.snakeyaml.lib.DumperOptions;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.context.ApplicationContextInitializer;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
public class ConfigInitializer
|
||||
implements ApplicationContextInitializer<ConfigurableApplicationContext> {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ConfigInitializer.class);
|
||||
|
||||
@Override
|
||||
public void initialize(ConfigurableApplicationContext applicationContext) {
|
||||
try {
|
||||
@@ -149,7 +148,7 @@ public class ConfigInitializer
|
||||
.commentSide(settingsTemplateFile.getComment(path, CommentType.SIDE));
|
||||
} else {
|
||||
// Log if the key is not found in both YAML files
|
||||
logger.info("Key not found in both YAML files: " + path);
|
||||
log.info("Key not found in both YAML files: " + path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,19 +7,19 @@ import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.context.annotation.DependsOn;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.SPDF.model.ApplicationProperties;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
@DependsOn({"bookAndHtmlFormatsInstalled"})
|
||||
public class EndpointConfiguration {
|
||||
private static final Logger logger = LoggerFactory.getLogger(EndpointConfiguration.class);
|
||||
|
||||
private Map<String, Boolean> endpointStatuses = new ConcurrentHashMap<>();
|
||||
private Map<String, Set<String>> endpointGroups = new ConcurrentHashMap<>();
|
||||
|
||||
@@ -43,7 +43,7 @@ public class EndpointConfiguration {
|
||||
|
||||
public void disableEndpoint(String endpoint) {
|
||||
if (!endpointStatuses.containsKey(endpoint) || endpointStatuses.get(endpoint) != false) {
|
||||
logger.debug("Disabling {}", endpoint);
|
||||
log.debug("Disabling {}", endpoint);
|
||||
endpointStatuses.put(endpoint, false);
|
||||
}
|
||||
}
|
||||
@@ -87,7 +87,7 @@ public class EndpointConfiguration {
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (!disabledList.isEmpty()) {
|
||||
logger.info(
|
||||
log.info(
|
||||
"Total disabled endpoints: {}. Disabled endpoints: {}",
|
||||
disabledList.size(),
|
||||
String.join(", ", disabledList));
|
||||
@@ -188,7 +188,7 @@ public class EndpointConfiguration {
|
||||
addEndpointToGroup("OpenCV", "extract-image-scans");
|
||||
|
||||
// LibreOffice
|
||||
addEndpointToGroup("LibreOffice", "repair");
|
||||
addEndpointToGroup("qpdf", "repair");
|
||||
addEndpointToGroup("LibreOffice", "file-to-pdf");
|
||||
addEndpointToGroup("LibreOffice", "pdf-to-word");
|
||||
addEndpointToGroup("LibreOffice", "pdf-to-presentation");
|
||||
@@ -199,10 +199,11 @@ public class EndpointConfiguration {
|
||||
// Unoconv
|
||||
addEndpointToGroup("Unoconv", "file-to-pdf");
|
||||
|
||||
// OCRmyPDF
|
||||
addEndpointToGroup("OCRmyPDF", "compress-pdf");
|
||||
addEndpointToGroup("OCRmyPDF", "pdf-to-pdfa");
|
||||
addEndpointToGroup("OCRmyPDF", "ocr-pdf");
|
||||
// qpdf
|
||||
addEndpointToGroup("qpdf", "compress-pdf");
|
||||
addEndpointToGroup("qpdf", "pdf-to-pdfa");
|
||||
|
||||
addEndpointToGroup("tesseract", "ocr-pdf");
|
||||
|
||||
// Java
|
||||
addEndpointToGroup("Java", "merge-pdfs");
|
||||
@@ -248,10 +249,10 @@ public class EndpointConfiguration {
|
||||
addEndpointToGroup("Javascript", "compare");
|
||||
addEndpointToGroup("Javascript", "adjust-contrast");
|
||||
|
||||
// Ghostscript dependent endpoints
|
||||
addEndpointToGroup("Ghostscript", "compress-pdf");
|
||||
addEndpointToGroup("Ghostscript", "pdf-to-pdfa");
|
||||
addEndpointToGroup("Ghostscript", "repair");
|
||||
// qpdf dependent endpoints
|
||||
addEndpointToGroup("qpdf", "compress-pdf");
|
||||
addEndpointToGroup("qpdf", "pdf-to-pdfa");
|
||||
addEndpointToGroup("qpdf", "repair");
|
||||
|
||||
// Weasyprint dependent endpoints
|
||||
addEndpointToGroup("Weasyprint", "html-to-pdf");
|
||||
@@ -259,6 +260,9 @@ public class EndpointConfiguration {
|
||||
|
||||
// Pdftohtml dependent endpoints
|
||||
addEndpointToGroup("Pdftohtml", "pdf-to-html");
|
||||
|
||||
// disabled for now while we resolve issues
|
||||
disableEndpoint("pdf-to-pdfa");
|
||||
}
|
||||
|
||||
private void processEnvironmentConfigs() {
|
||||
|
||||
@@ -37,12 +37,12 @@ public class ExternalAppDepConfig {
|
||||
private final Map<String, List<String>> commandToGroupMapping =
|
||||
new HashMap<>() {
|
||||
{
|
||||
put("gs", List.of("Ghostscript"));
|
||||
put("soffice", List.of("LibreOffice"));
|
||||
put("ocrmypdf", List.of("OCRmyPDF"));
|
||||
put("weasyprint", List.of("Weasyprint"));
|
||||
put("pdftohtml", List.of("Pdftohtml"));
|
||||
put("unoconv", List.of("Unoconv"));
|
||||
put("qpdf", List.of("qpdf"));
|
||||
put("tesseract", List.of("tesseract"));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -97,9 +97,9 @@ public class ExternalAppDepConfig {
|
||||
public void checkDependencies() {
|
||||
|
||||
// Check core dependencies
|
||||
checkDependencyAndDisableGroup("gs");
|
||||
checkDependencyAndDisableGroup("tesseract");
|
||||
checkDependencyAndDisableGroup("soffice");
|
||||
checkDependencyAndDisableGroup("ocrmypdf");
|
||||
checkDependencyAndDisableGroup("qpdf");
|
||||
checkDependencyAndDisableGroup("weasyprint");
|
||||
checkDependencyAndDisableGroup("pdftohtml");
|
||||
checkDependencyAndDisableGroup("unoconv");
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
package stirling.software.SPDF.config;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Properties;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import io.micrometer.common.util.StringUtils;
|
||||
@@ -23,6 +26,18 @@ public class InitialSetup {
|
||||
@Autowired private ApplicationProperties applicationProperties;
|
||||
|
||||
@PostConstruct
|
||||
public void init() throws IOException {
|
||||
initUUIDKey();
|
||||
|
||||
initSecretKey();
|
||||
|
||||
initEnableCSRFSecurity();
|
||||
|
||||
initLegalUrls();
|
||||
|
||||
initSetAppVersion();
|
||||
}
|
||||
|
||||
public void initUUIDKey() throws IOException {
|
||||
String uuid = applicationProperties.getAutomaticallyGenerated().getUUID();
|
||||
if (!GeneralUtils.isValidUUID(uuid)) {
|
||||
@@ -32,7 +47,6 @@ public class InitialSetup {
|
||||
}
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void initSecretKey() throws IOException {
|
||||
String secretKey = applicationProperties.getAutomaticallyGenerated().getKey();
|
||||
if (!GeneralUtils.isValidUUID(secretKey)) {
|
||||
@@ -42,13 +56,24 @@ public class InitialSetup {
|
||||
}
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void initEnableCSRFSecurity() throws IOException {
|
||||
if (GeneralUtils.isVersionHigher(
|
||||
"0.36.0", applicationProperties.getAutomaticallyGenerated().getAppVersion())) {
|
||||
Boolean csrf = applicationProperties.getSecurity().getCsrfDisabled();
|
||||
if (!csrf) {
|
||||
GeneralUtils.saveKeyToConfig("security.csrfDisabled", false, false);
|
||||
GeneralUtils.saveKeyToConfig("system.enableAnalytics", "true", false);
|
||||
applicationProperties.getSecurity().setCsrfDisabled(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void initLegalUrls() throws IOException {
|
||||
// Initialize Terms and Conditions
|
||||
String termsUrl = applicationProperties.getLegal().getTermsAndConditions();
|
||||
if (StringUtils.isEmpty(termsUrl)) {
|
||||
String defaultTermsUrl = "https://www.stirlingpdf.com/terms-and-conditions";
|
||||
GeneralUtils.saveKeyToConfig("legal.termsAndConditions", defaultTermsUrl);
|
||||
GeneralUtils.saveKeyToConfig("legal.termsAndConditions", defaultTermsUrl, false);
|
||||
applicationProperties.getLegal().setTermsAndConditions(defaultTermsUrl);
|
||||
}
|
||||
|
||||
@@ -56,8 +81,23 @@ public class InitialSetup {
|
||||
String privacyUrl = applicationProperties.getLegal().getPrivacyPolicy();
|
||||
if (StringUtils.isEmpty(privacyUrl)) {
|
||||
String defaultPrivacyUrl = "https://www.stirlingpdf.com/privacy-policy";
|
||||
GeneralUtils.saveKeyToConfig("legal.privacyPolicy", defaultPrivacyUrl);
|
||||
GeneralUtils.saveKeyToConfig("legal.privacyPolicy", defaultPrivacyUrl, false);
|
||||
applicationProperties.getLegal().setPrivacyPolicy(defaultPrivacyUrl);
|
||||
}
|
||||
}
|
||||
|
||||
public void initSetAppVersion() throws IOException {
|
||||
|
||||
String appVersion = "0.0.0";
|
||||
Resource resource = new ClassPathResource("version.properties");
|
||||
Properties props = new Properties();
|
||||
try {
|
||||
props.load(resource.getInputStream());
|
||||
appVersion = props.getProperty("version");
|
||||
} catch (Exception e) {
|
||||
|
||||
}
|
||||
applicationProperties.getAutomaticallyGenerated().setAppVersion(appVersion);
|
||||
GeneralUtils.saveKeyToConfig("AutomaticallyGenerated.appVersion", appVersion, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import java.util.List;
|
||||
import stirling.software.SPDF.utils.FileInfo;
|
||||
|
||||
public interface DatabaseBackupInterface {
|
||||
|
||||
void exportDatabase() throws IOException;
|
||||
|
||||
boolean importDatabase();
|
||||
|
||||
@@ -30,6 +30,7 @@ public class InitialSecuritySetup {
|
||||
initializeAdminUser();
|
||||
} else {
|
||||
databaseBackupHelper.exportDatabase();
|
||||
userService.migrateOauth2ToSSO();
|
||||
}
|
||||
initializeInternalApiUser();
|
||||
}
|
||||
@@ -74,5 +75,6 @@ public class InitialSecuritySetup {
|
||||
userService.addApiKeyToUser(Role.INTERNAL_API_USER.getRoleId());
|
||||
log.info("Internal API user created: " + Role.INTERNAL_API_USER.getRoleId());
|
||||
}
|
||||
userService.syncCustomApiUser(applicationProperties.getSecurity().getCustomGlobalAPIKey());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,14 +3,16 @@ package stirling.software.SPDF.config.security;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.*;
|
||||
|
||||
import org.opensaml.saml.saml2.core.AuthnRequest;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
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.Configuration;
|
||||
import org.springframework.context.annotation.DependsOn;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.authentication.ProviderManager;
|
||||
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
@@ -32,7 +34,8 @@ import org.springframework.security.saml2.provider.service.authentication.OpenSa
|
||||
import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
|
||||
import org.springframework.security.saml2.provider.service.web.authentication.Saml2WebSsoAuthenticationFilter;
|
||||
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
|
||||
import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
|
||||
@@ -41,6 +44,7 @@ import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
|
||||
import org.springframework.security.web.savedrequest.NullRequestCache;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationFailureHandler;
|
||||
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationSuccessHandler;
|
||||
@@ -64,6 +68,7 @@ import stirling.software.SPDF.repository.JPATokenRepositoryImpl;
|
||||
@EnableWebSecurity
|
||||
@EnableMethodSecurity
|
||||
@Slf4j
|
||||
@DependsOn("runningEE")
|
||||
public class SecurityConfiguration {
|
||||
|
||||
@Autowired private CustomUserDetailsService userDetailsService;
|
||||
@@ -79,6 +84,10 @@ public class SecurityConfiguration {
|
||||
@Qualifier("loginEnabled")
|
||||
public boolean loginEnabledValue;
|
||||
|
||||
@Autowired
|
||||
@Qualifier("runningEE")
|
||||
public boolean runningEE;
|
||||
|
||||
@Autowired ApplicationProperties applicationProperties;
|
||||
|
||||
@Autowired private UserAuthenticationFilter userAuthenticationFilter;
|
||||
@@ -90,13 +99,14 @@ public class SecurityConfiguration {
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
if (applicationProperties.getSecurity().getCsrfDisabled() || !loginEnabledValue) {
|
||||
http.csrf(csrf -> csrf.disable());
|
||||
}
|
||||
|
||||
if (loginEnabledValue) {
|
||||
http.addFilterBefore(
|
||||
userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
||||
if (applicationProperties.getSecurity().getCsrfDisabled()) {
|
||||
http.csrf(csrf -> csrf.disable());
|
||||
} else {
|
||||
if (!applicationProperties.getSecurity().getCsrfDisabled()) {
|
||||
CookieCsrfTokenRepository cookieRepo =
|
||||
CookieCsrfTokenRepository.withHttpOnlyFalse();
|
||||
CsrfTokenRequestAttributeHandler requestHandler =
|
||||
@@ -106,7 +116,7 @@ public class SecurityConfiguration {
|
||||
csrf ->
|
||||
csrf.ignoringRequestMatchers(
|
||||
request -> {
|
||||
String apiKey = request.getHeader("X-API-Key");
|
||||
String apiKey = request.getHeader("X-API-KEY");
|
||||
|
||||
// If there's no API key, don't ignore CSRF
|
||||
// (return false)
|
||||
@@ -245,12 +255,22 @@ public class SecurityConfiguration {
|
||||
}
|
||||
|
||||
// Handle SAML
|
||||
if (applicationProperties.getSecurity().isSaml2Activ()
|
||||
&& applicationProperties.getSystem().getEnableAlphaFunctionality()) {
|
||||
http.authenticationProvider(samlAuthenticationProvider());
|
||||
http.saml2Login(
|
||||
saml2 ->
|
||||
if (applicationProperties.getSecurity().isSaml2Activ()) { // && runningEE
|
||||
// Configure the authentication provider
|
||||
OpenSaml4AuthenticationProvider authenticationProvider =
|
||||
new OpenSaml4AuthenticationProvider();
|
||||
authenticationProvider.setResponseAuthenticationConverter(
|
||||
new CustomSaml2ResponseAuthenticationConverter(userService));
|
||||
|
||||
http.authenticationProvider(authenticationProvider)
|
||||
.saml2Login(
|
||||
saml2 -> {
|
||||
try {
|
||||
saml2.loginPage("/saml2")
|
||||
.relyingPartyRegistrationRepository(
|
||||
relyingPartyRegistrations())
|
||||
.authenticationManager(
|
||||
new ProviderManager(authenticationProvider))
|
||||
.successHandler(
|
||||
new CustomSaml2AuthenticationSuccessHandler(
|
||||
loginAttemptService,
|
||||
@@ -258,44 +278,34 @@ public class SecurityConfiguration {
|
||||
userService))
|
||||
.failureHandler(
|
||||
new CustomSaml2AuthenticationFailureHandler())
|
||||
.permitAll())
|
||||
.addFilterBefore(
|
||||
userAuthenticationFilter, Saml2WebSsoAuthenticationFilter.class);
|
||||
.authenticationRequestResolver(
|
||||
authenticationRequestResolver(
|
||||
relyingPartyRegistrations()));
|
||||
} catch (Exception e) {
|
||||
log.error("Error configuring SAML2 login", e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
} else {
|
||||
if (applicationProperties.getSecurity().getCsrfDisabled()) {
|
||||
http.csrf(csrf -> csrf.disable());
|
||||
} else {
|
||||
CookieCsrfTokenRepository cookieRepo =
|
||||
CookieCsrfTokenRepository.withHttpOnlyFalse();
|
||||
CsrfTokenRequestAttributeHandler requestHandler =
|
||||
new CsrfTokenRequestAttributeHandler();
|
||||
requestHandler.setCsrfRequestAttributeName(null);
|
||||
http.csrf(
|
||||
csrf ->
|
||||
csrf.csrfTokenRepository(cookieRepo)
|
||||
.csrfTokenRequestHandler(requestHandler));
|
||||
}
|
||||
// if (!applicationProperties.getSecurity().getCsrfDisabled()) {
|
||||
// CookieCsrfTokenRepository cookieRepo =
|
||||
// CookieCsrfTokenRepository.withHttpOnlyFalse();
|
||||
// CsrfTokenRequestAttributeHandler requestHandler =
|
||||
// new CsrfTokenRequestAttributeHandler();
|
||||
// requestHandler.setCsrfRequestAttributeName(null);
|
||||
// http.csrf(
|
||||
// csrf ->
|
||||
// csrf.csrfTokenRepository(cookieRepo)
|
||||
// .csrfTokenRequestHandler(requestHandler));
|
||||
// }
|
||||
http.authorizeHttpRequests(authz -> authz.anyRequest().permitAll());
|
||||
}
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnProperty(
|
||||
name = "security.saml2.enabled",
|
||||
havingValue = "true",
|
||||
matchIfMissing = false)
|
||||
public AuthenticationProvider samlAuthenticationProvider() {
|
||||
OpenSaml4AuthenticationProvider authenticationProvider =
|
||||
new OpenSaml4AuthenticationProvider();
|
||||
authenticationProvider.setResponseAuthenticationConverter(
|
||||
new CustomSaml2ResponseAuthenticationConverter(userService));
|
||||
return authenticationProvider;
|
||||
}
|
||||
|
||||
// Client Registration Repository for OAUTH2 OIDC Login
|
||||
@Bean
|
||||
@ConditionalOnProperty(
|
||||
value = "security.oauth2.enabled",
|
||||
@@ -432,11 +442,12 @@ public class SecurityConfiguration {
|
||||
havingValue = "true",
|
||||
matchIfMissing = false)
|
||||
public RelyingPartyRegistrationRepository relyingPartyRegistrations() throws Exception {
|
||||
|
||||
SAML2 samlConf = applicationProperties.getSecurity().getSaml2();
|
||||
|
||||
Resource privateKeyResource = samlConf.getPrivateKey();
|
||||
X509Certificate idpCert = CertificateUtils.readCertificate(samlConf.getidpCert());
|
||||
Saml2X509Credential verificationCredential = Saml2X509Credential.verification(idpCert);
|
||||
|
||||
Resource privateKeyResource = samlConf.getPrivateKey();
|
||||
Resource certificateResource = samlConf.getSpCert();
|
||||
|
||||
Saml2X509Credential signingCredential =
|
||||
@@ -445,26 +456,97 @@ public class SecurityConfiguration {
|
||||
CertificateUtils.readCertificate(certificateResource),
|
||||
Saml2X509CredentialType.SIGNING);
|
||||
|
||||
X509Certificate idpCert = CertificateUtils.readCertificate(samlConf.getidpCert());
|
||||
|
||||
Saml2X509Credential verificationCredential = Saml2X509Credential.verification(idpCert);
|
||||
|
||||
RelyingPartyRegistration rp =
|
||||
RelyingPartyRegistration.withRegistrationId(samlConf.getRegistrationId())
|
||||
.signingX509Credentials((c) -> c.add(signingCredential))
|
||||
.assertingPartyDetails(
|
||||
(details) ->
|
||||
details.entityId(samlConf.getIdpIssuer())
|
||||
.signingX509Credentials(c -> c.add(signingCredential))
|
||||
.assertingPartyMetadata(
|
||||
metadata ->
|
||||
metadata.entityId(samlConf.getIdpIssuer())
|
||||
.singleSignOnServiceLocation(
|
||||
samlConf.getIdpSingleLoginUrl())
|
||||
.verificationX509Credentials(
|
||||
(c) -> c.add(verificationCredential))
|
||||
c -> c.add(verificationCredential))
|
||||
.singleSignOnServiceBinding(
|
||||
Saml2MessageBinding.POST)
|
||||
.wantAuthnRequestsSigned(true))
|
||||
.build();
|
||||
|
||||
return new InMemoryRelyingPartyRegistrationRepository(rp);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnProperty(
|
||||
name = "security.saml2.enabled",
|
||||
havingValue = "true",
|
||||
matchIfMissing = false)
|
||||
public OpenSaml4AuthenticationRequestResolver authenticationRequestResolver(
|
||||
RelyingPartyRegistrationRepository relyingPartyRegistrationRepository) {
|
||||
OpenSaml4AuthenticationRequestResolver resolver =
|
||||
new OpenSaml4AuthenticationRequestResolver(relyingPartyRegistrationRepository);
|
||||
resolver.setAuthnRequestCustomizer(
|
||||
customizer -> {
|
||||
log.debug("Customizing SAML Authentication request");
|
||||
|
||||
AuthnRequest authnRequest = customizer.getAuthnRequest();
|
||||
log.debug("AuthnRequest ID: {}", authnRequest.getID());
|
||||
|
||||
if (authnRequest.getID() == null) {
|
||||
authnRequest.setID("ARQ" + UUID.randomUUID().toString());
|
||||
}
|
||||
log.debug("AuthnRequest new ID after set: {}", authnRequest.getID());
|
||||
log.debug("AuthnRequest IssueInstant: {}", authnRequest.getIssueInstant());
|
||||
log.debug(
|
||||
"AuthnRequest Issuer: {}",
|
||||
authnRequest.getIssuer() != null
|
||||
? authnRequest.getIssuer().getValue()
|
||||
: "null");
|
||||
|
||||
HttpServletRequest request = customizer.getRequest();
|
||||
|
||||
// Log HTTP request details
|
||||
log.debug("HTTP Request Method: {}", request.getMethod());
|
||||
log.debug("Request URI: {}", request.getRequestURI());
|
||||
log.debug("Request URL: {}", request.getRequestURL().toString());
|
||||
log.debug("Query String: {}", request.getQueryString());
|
||||
log.debug("Remote Address: {}", request.getRemoteAddr());
|
||||
|
||||
// Log headers
|
||||
Collections.list(request.getHeaderNames())
|
||||
.forEach(
|
||||
headerName -> {
|
||||
log.debug(
|
||||
"Header - {}: {}",
|
||||
headerName,
|
||||
request.getHeader(headerName));
|
||||
});
|
||||
|
||||
// Log SAML specific parameters
|
||||
log.debug("SAML Request Parameters:");
|
||||
log.debug("SAMLRequest: {}", request.getParameter("SAMLRequest"));
|
||||
log.debug("RelayState: {}", request.getParameter("RelayState"));
|
||||
|
||||
// Log session debugrmation if exists
|
||||
if (request.getSession(false) != null) {
|
||||
log.debug("Session ID: {}", request.getSession().getId());
|
||||
}
|
||||
|
||||
// Log any assertions consumer service details if present
|
||||
if (authnRequest.getAssertionConsumerServiceURL() != null) {
|
||||
log.debug(
|
||||
"AssertionConsumerServiceURL: {}",
|
||||
authnRequest.getAssertionConsumerServiceURL());
|
||||
}
|
||||
|
||||
// Log NameID policy if present
|
||||
if (authnRequest.getNameIDPolicy() != null) {
|
||||
log.debug(
|
||||
"NameIDPolicy Format: {}",
|
||||
authnRequest.getNameIDPolicy().getFormat());
|
||||
}
|
||||
});
|
||||
return resolver;
|
||||
}
|
||||
|
||||
public DaoAuthenticationProvider daoAuthenticationProvider() {
|
||||
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
|
||||
provider.setUserDetailsService(userDetailsService);
|
||||
|
||||
@@ -71,7 +71,7 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
|
||||
|
||||
// Check for API key in the request headers if no authentication exists
|
||||
if (authentication == null || !authentication.isAuthenticated()) {
|
||||
String apiKey = request.getHeader("X-API-Key");
|
||||
String apiKey = request.getHeader("X-API-KEY");
|
||||
if (apiKey != null && !apiKey.trim().isEmpty()) {
|
||||
try {
|
||||
// Use API key to authenticate. This requires you to have an authentication
|
||||
|
||||
@@ -59,7 +59,7 @@ public class UserBasedRateLimitingFilter extends OncePerRequestFilter {
|
||||
String identifier = null;
|
||||
|
||||
// Check for API key in the request headers
|
||||
String apiKey = request.getHeader("X-API-Key");
|
||||
String apiKey = request.getHeader("X-API-KEY");
|
||||
if (apiKey != null && !apiKey.trim().isEmpty()) {
|
||||
identifier =
|
||||
"API_KEY_" + apiKey; // Prefix to distinguish between API keys and usernames
|
||||
@@ -79,7 +79,7 @@ public class UserBasedRateLimitingFilter extends OncePerRequestFilter {
|
||||
Role userRole =
|
||||
getRoleFromAuthentication(SecurityContextHolder.getContext().getAuthentication());
|
||||
|
||||
if (request.getHeader("X-API-Key") != null) {
|
||||
if (request.getHeader("X-API-KEY") != null) {
|
||||
// It's an API call
|
||||
processRequest(
|
||||
userRole.getApiCallsPerDay(),
|
||||
|
||||
@@ -18,6 +18,7 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.SPDF.config.interfaces.DatabaseBackupInterface;
|
||||
@@ -50,8 +51,19 @@ public class UserService implements UserServiceInterface {
|
||||
|
||||
@Autowired ApplicationProperties applicationProperties;
|
||||
|
||||
@Transactional
|
||||
public void migrateOauth2ToSSO() {
|
||||
userRepository
|
||||
.findByAuthenticationTypeIgnoreCase("OAUTH2")
|
||||
.forEach(
|
||||
user -> {
|
||||
user.setAuthenticationType(AuthenticationType.SSO);
|
||||
userRepository.save(user);
|
||||
});
|
||||
}
|
||||
|
||||
// Handle OAUTH2 login and user auto creation.
|
||||
public boolean processOAuth2PostLogin(String username, boolean autoCreateUser)
|
||||
public boolean processSSOPostLogin(String username, boolean autoCreateUser)
|
||||
throws IllegalArgumentException, IOException {
|
||||
if (!isUsernameValid(username)) {
|
||||
return false;
|
||||
@@ -61,7 +73,7 @@ public class UserService implements UserServiceInterface {
|
||||
return true;
|
||||
}
|
||||
if (autoCreateUser) {
|
||||
saveUser(username, AuthenticationType.OAUTH2);
|
||||
saveUser(username, AuthenticationType.SSO);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -378,6 +390,37 @@ public class UserService implements UserServiceInterface {
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void syncCustomApiUser(String customApiKey) throws IOException {
|
||||
if (customApiKey == null || customApiKey.trim().length() == 0) {
|
||||
return;
|
||||
}
|
||||
String username = "CUSTOM_API_USER";
|
||||
Optional<User> existingUser = findByUsernameIgnoreCase(username);
|
||||
|
||||
if (!existingUser.isPresent()) {
|
||||
// Create new user with API role
|
||||
User user = new User();
|
||||
user.setUsername(username);
|
||||
user.setPassword(UUID.randomUUID().toString());
|
||||
user.setEnabled(true);
|
||||
user.setFirstLogin(false);
|
||||
user.setAuthenticationType(AuthenticationType.WEB);
|
||||
user.setApiKey(customApiKey);
|
||||
user.addAuthority(new Authority(Role.INTERNAL_API_USER.getRoleId(), user));
|
||||
userRepository.save(user);
|
||||
databaseBackupHelper.exportDatabase();
|
||||
} else {
|
||||
// Update API key if it has changed
|
||||
User user = existingUser.get();
|
||||
if (!customApiKey.equals(user.getApiKey())) {
|
||||
user.setApiKey(customApiKey);
|
||||
userRepository.save(user);
|
||||
databaseBackupHelper.exportDatabase();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getTotalUsersCount() {
|
||||
return userRepository.count();
|
||||
|
||||
@@ -34,6 +34,12 @@ public class DatabaseBackupHelper implements DatabaseBackupInterface {
|
||||
@Value("${spring.datasource.url}")
|
||||
private String url;
|
||||
|
||||
@Value("${spring.datasource.username}")
|
||||
private String databaseUsername;
|
||||
|
||||
@Value("${spring.datasource.password}")
|
||||
private String databasePassword;
|
||||
|
||||
private Path backupPath = Paths.get("configs/db/backup/");
|
||||
|
||||
@Override
|
||||
@@ -134,7 +140,8 @@ public class DatabaseBackupHelper implements DatabaseBackupInterface {
|
||||
this.getBackupFilePath("backup_" + dateNow.format(myFormatObj) + ".sql");
|
||||
String query = "SCRIPT SIMPLE COLUMNS DROP to ?;";
|
||||
|
||||
try (Connection conn = DriverManager.getConnection(url, "sa", "");
|
||||
try (Connection conn =
|
||||
DriverManager.getConnection(url, databaseUsername, databasePassword);
|
||||
PreparedStatement stmt = conn.prepareStatement(query)) {
|
||||
stmt.setString(1, insertOutputFilePath.toString());
|
||||
stmt.execute();
|
||||
@@ -147,7 +154,8 @@ public class DatabaseBackupHelper implements DatabaseBackupInterface {
|
||||
// Retrieves the H2 database version.
|
||||
public String getH2Version() {
|
||||
String version = "Unknown";
|
||||
try (Connection conn = DriverManager.getConnection(url, "sa", "")) {
|
||||
try (Connection conn =
|
||||
DriverManager.getConnection(url, databaseUsername, databasePassword)) {
|
||||
try (Statement stmt = conn.createStatement();
|
||||
ResultSet rs = stmt.executeQuery("SELECT H2VERSION() AS version")) {
|
||||
if (rs.next()) {
|
||||
@@ -189,7 +197,8 @@ public class DatabaseBackupHelper implements DatabaseBackupInterface {
|
||||
private boolean executeDatabaseScript(Path scriptPath) {
|
||||
String query = "RUNSCRIPT from ?;";
|
||||
|
||||
try (Connection conn = DriverManager.getConnection(url, "sa", "");
|
||||
try (Connection conn =
|
||||
DriverManager.getConnection(url, databaseUsername, databasePassword);
|
||||
PreparedStatement stmt = conn.prepareStatement(query)) {
|
||||
stmt.setString(1, scriptPath.toString());
|
||||
stmt.execute();
|
||||
|
||||
@@ -82,8 +82,7 @@ public class CustomOAuth2AuthenticationSuccessHandler
|
||||
}
|
||||
if (userService.usernameExistsIgnoreCase(username)
|
||||
&& userService.hasPassword(username)
|
||||
&& !userService.isAuthenticationTypeByUsername(
|
||||
username, AuthenticationType.OAUTH2)
|
||||
&& !userService.isAuthenticationTypeByUsername(username, AuthenticationType.SSO)
|
||||
&& oAuth.getAutoCreateUser()) {
|
||||
response.sendRedirect(contextPath + "/logout?oauth2AuthenticationErrorWeb=true");
|
||||
return;
|
||||
@@ -95,7 +94,7 @@ public class CustomOAuth2AuthenticationSuccessHandler
|
||||
return;
|
||||
}
|
||||
if (principal instanceof OAuth2User) {
|
||||
userService.processOAuth2PostLogin(username, oAuth.getAutoCreateUser());
|
||||
userService.processSSOPostLogin(username, oAuth.getAutoCreateUser());
|
||||
}
|
||||
response.sendRedirect(contextPath + "/");
|
||||
return;
|
||||
|
||||
@@ -2,8 +2,6 @@ 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;
|
||||
@@ -13,6 +11,7 @@ import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
|
||||
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.SPDF.config.security.LoginAttemptService;
|
||||
import stirling.software.SPDF.config.security.UserService;
|
||||
import stirling.software.SPDF.model.ApplicationProperties;
|
||||
@@ -20,6 +19,7 @@ import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
|
||||
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2.Client;
|
||||
import stirling.software.SPDF.model.User;
|
||||
|
||||
@Slf4j
|
||||
public class CustomOAuth2UserService implements OAuth2UserService<OidcUserRequest, OidcUser> {
|
||||
|
||||
private final OidcUserService delegate = new OidcUserService();
|
||||
@@ -30,8 +30,6 @@ public class CustomOAuth2UserService implements OAuth2UserService<OidcUserReques
|
||||
|
||||
private ApplicationProperties applicationProperties;
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(CustomOAuth2UserService.class);
|
||||
|
||||
public CustomOAuth2UserService(
|
||||
ApplicationProperties applicationProperties,
|
||||
UserService userService,
|
||||
@@ -82,10 +80,10 @@ public class CustomOAuth2UserService implements OAuth2UserService<OidcUserReques
|
||||
user.getUserInfo(),
|
||||
usernameAttribute);
|
||||
} catch (IllegalArgumentException e) {
|
||||
logger.error("Error loading OIDC user: {}", e.getMessage());
|
||||
log.error("Error loading OIDC user: {}", e.getMessage());
|
||||
throw new OAuth2AuthenticationException(new OAuth2Error(e.getMessage()), e);
|
||||
} catch (Exception e) {
|
||||
logger.error("Unexpected error loading OIDC user", e);
|
||||
log.error("Unexpected error loading OIDC user", e);
|
||||
throw new OAuth2AuthenticationException("Unexpected error during authentication");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,14 @@ package stirling.software.SPDF.config.security.saml2;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.security.interfaces.RSAPrivateKey;
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
|
||||
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
|
||||
import org.bouncycastle.openssl.PEMKeyPair;
|
||||
import org.bouncycastle.openssl.PEMParser;
|
||||
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
|
||||
import org.bouncycastle.util.io.pem.PemObject;
|
||||
import org.bouncycastle.util.io.pem.PemReader;
|
||||
import org.springframework.core.io.Resource;
|
||||
@@ -28,15 +30,26 @@ public class CertificateUtils {
|
||||
}
|
||||
|
||||
public static RSAPrivateKey readPrivateKey(Resource privateKeyResource) throws Exception {
|
||||
try (PemReader pemReader =
|
||||
new PemReader(
|
||||
try (PEMParser pemParser =
|
||||
new PEMParser(
|
||||
new InputStreamReader(
|
||||
privateKeyResource.getInputStream(), StandardCharsets.UTF_8))) {
|
||||
PemObject pemObject = pemReader.readPemObject();
|
||||
byte[] decodedKey = pemObject.getContent();
|
||||
return (RSAPrivateKey)
|
||||
KeyFactory.getInstance("RSA")
|
||||
.generatePrivate(new PKCS8EncodedKeySpec(decodedKey));
|
||||
|
||||
Object object = pemParser.readObject();
|
||||
JcaPEMKeyConverter converter = new JcaPEMKeyConverter();
|
||||
|
||||
if (object instanceof PEMKeyPair) {
|
||||
// Handle traditional RSA private key format
|
||||
PEMKeyPair keypair = (PEMKeyPair) object;
|
||||
return (RSAPrivateKey) converter.getPrivateKey(keypair.getPrivateKeyInfo());
|
||||
} else if (object instanceof PrivateKeyInfo) {
|
||||
// Handle PKCS#8 format
|
||||
return (RSAPrivateKey) converter.getPrivateKey((PrivateKeyInfo) object);
|
||||
} else {
|
||||
throw new IllegalArgumentException(
|
||||
"Unsupported key format: "
|
||||
+ (object != null ? object.getClass().getName() : "null"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.SPDF.config.security.LoginAttemptService;
|
||||
import stirling.software.SPDF.config.security.UserService;
|
||||
import stirling.software.SPDF.model.ApplicationProperties;
|
||||
@@ -20,11 +21,11 @@ import stirling.software.SPDF.model.AuthenticationType;
|
||||
import stirling.software.SPDF.utils.RequestUriUtils;
|
||||
|
||||
@AllArgsConstructor
|
||||
@Slf4j
|
||||
public class CustomSaml2AuthenticationSuccessHandler
|
||||
extends SavedRequestAwareAuthenticationSuccessHandler {
|
||||
|
||||
private LoginAttemptService loginAttemptService;
|
||||
|
||||
private ApplicationProperties applicationProperties;
|
||||
private UserService userService;
|
||||
|
||||
@@ -34,10 +35,12 @@ public class CustomSaml2AuthenticationSuccessHandler
|
||||
throws ServletException, IOException {
|
||||
|
||||
Object principal = authentication.getPrincipal();
|
||||
log.debug("Starting SAML2 authentication success handling");
|
||||
|
||||
if (principal instanceof CustomSaml2AuthenticatedPrincipal) {
|
||||
String username = ((CustomSaml2AuthenticatedPrincipal) principal).getName();
|
||||
// Get the saved request
|
||||
log.debug("Authenticated principal found for user: {}", username);
|
||||
|
||||
HttpSession session = request.getSession(false);
|
||||
String contextPath = request.getContextPath();
|
||||
SavedRequest savedRequest =
|
||||
@@ -45,46 +48,77 @@ public class CustomSaml2AuthenticationSuccessHandler
|
||||
? (SavedRequest) session.getAttribute("SPRING_SECURITY_SAVED_REQUEST")
|
||||
: null;
|
||||
|
||||
log.debug(
|
||||
"Session exists: {}, Saved request exists: {}",
|
||||
session != null,
|
||||
savedRequest != null);
|
||||
|
||||
if (savedRequest != null
|
||||
&& !RequestUriUtils.isStaticResource(
|
||||
contextPath, savedRequest.getRedirectUrl())) {
|
||||
// Redirect to the original destination
|
||||
log.debug(
|
||||
"Valid saved request found, redirecting to original destination: {}",
|
||||
savedRequest.getRedirectUrl());
|
||||
super.onAuthenticationSuccess(request, response, authentication);
|
||||
} else {
|
||||
SAML2 saml2 = applicationProperties.getSecurity().getSaml2();
|
||||
log.debug(
|
||||
"Processing SAML2 authentication with autoCreateUser: {}",
|
||||
saml2.getAutoCreateUser());
|
||||
|
||||
if (loginAttemptService.isBlocked(username)) {
|
||||
log.debug("User {} is blocked due to too many login attempts", 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)
|
||||
&& saml2.getAutoCreateUser()) {
|
||||
|
||||
boolean userExists = userService.usernameExistsIgnoreCase(username);
|
||||
boolean hasPassword = userExists && userService.hasPassword(username);
|
||||
boolean isSSOUser =
|
||||
userExists
|
||||
&& userService.isAuthenticationTypeByUsername(
|
||||
username, AuthenticationType.SSO);
|
||||
|
||||
log.debug(
|
||||
"User status - Exists: {}, Has password: {}, Is SSO user: {}",
|
||||
userExists,
|
||||
hasPassword,
|
||||
isSSOUser);
|
||||
|
||||
if (userExists && hasPassword && !isSSOUser && saml2.getAutoCreateUser()) {
|
||||
log.debug(
|
||||
"User {} exists with password but is not SSO user, redirecting to logout",
|
||||
username);
|
||||
response.sendRedirect(
|
||||
contextPath + "/logout?oauth2AuthenticationErrorWeb=true");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (saml2.getBlockRegistration()
|
||||
&& !userService.usernameExistsIgnoreCase(username)) {
|
||||
if (saml2.getBlockRegistration() && !userExists) {
|
||||
log.debug("Registration blocked for new user: {}", username);
|
||||
response.sendRedirect(
|
||||
contextPath + "/login?erroroauth=oauth2_admin_blocked_user");
|
||||
return;
|
||||
}
|
||||
userService.processOAuth2PostLogin(username, saml2.getAutoCreateUser());
|
||||
log.debug("Processing SSO post-login for user: {}", username);
|
||||
userService.processSSOPostLogin(username, saml2.getAutoCreateUser());
|
||||
log.debug("Successfully processed authentication for user: {}", username);
|
||||
response.sendRedirect(contextPath + "/");
|
||||
return;
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.debug(
|
||||
"Invalid username detected for user: {}, redirecting to logout",
|
||||
username);
|
||||
response.sendRedirect(contextPath + "/logout?invalidUsername=true");
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.debug("Non-SAML2 principal detected, delegating to parent handler");
|
||||
super.onAuthenticationSuccess(request, response, authentication);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,6 @@ package stirling.software.SPDF.config.security.saml2;
|
||||
import java.util.*;
|
||||
|
||||
import org.opensaml.core.xml.XMLObject;
|
||||
import org.opensaml.core.xml.schema.XSBoolean;
|
||||
import org.opensaml.core.xml.schema.XSString;
|
||||
import org.opensaml.saml.saml2.core.Assertion;
|
||||
import org.opensaml.saml.saml2.core.Attribute;
|
||||
import org.opensaml.saml.saml2.core.AttributeStatement;
|
||||
@@ -30,15 +28,60 @@ public class CustomSaml2ResponseAuthenticationConverter
|
||||
this.userService = userService;
|
||||
}
|
||||
|
||||
private Map<String, List<Object>> extractAttributes(Assertion assertion) {
|
||||
Map<String, List<Object>> attributes = new HashMap<>();
|
||||
|
||||
for (AttributeStatement attributeStatement : assertion.getAttributeStatements()) {
|
||||
for (Attribute attribute : attributeStatement.getAttributes()) {
|
||||
String attributeName = attribute.getName();
|
||||
List<Object> values = new ArrayList<>();
|
||||
|
||||
for (XMLObject xmlObject : attribute.getAttributeValues()) {
|
||||
// Get the text content directly
|
||||
String value = xmlObject.getDOM().getTextContent();
|
||||
if (value != null && !value.trim().isEmpty()) {
|
||||
values.add(value);
|
||||
}
|
||||
}
|
||||
|
||||
if (!values.isEmpty()) {
|
||||
// Store with both full URI and last part of the URI
|
||||
attributes.put(attributeName, values);
|
||||
String shortName = attributeName.substring(attributeName.lastIndexOf('/') + 1);
|
||||
attributes.put(shortName, values);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return attributes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Saml2Authentication convert(ResponseToken responseToken) {
|
||||
// Extract the assertion from the response
|
||||
Assertion assertion = responseToken.getResponse().getAssertions().get(0);
|
||||
Map<String, List<Object>> attributes = extractAttributes(assertion);
|
||||
|
||||
// Extract the NameID
|
||||
String nameId = assertion.getSubject().getNameID().getValue();
|
||||
// Debug log with actual values
|
||||
log.debug("Extracted SAML Attributes: " + attributes);
|
||||
|
||||
Optional<User> userOpt = userService.findByUsernameIgnoreCase(nameId);
|
||||
// Try to get username/identifier in order of preference
|
||||
String userIdentifier = null;
|
||||
if (hasAttribute(attributes, "username")) {
|
||||
userIdentifier = getFirstAttributeValue(attributes, "username");
|
||||
} else if (hasAttribute(attributes, "emailaddress")) {
|
||||
userIdentifier = getFirstAttributeValue(attributes, "emailaddress");
|
||||
} else if (hasAttribute(attributes, "name")) {
|
||||
userIdentifier = getFirstAttributeValue(attributes, "name");
|
||||
} else if (hasAttribute(attributes, "upn")) {
|
||||
userIdentifier = getFirstAttributeValue(attributes, "upn");
|
||||
} else if (hasAttribute(attributes, "uid")) {
|
||||
userIdentifier = getFirstAttributeValue(attributes, "uid");
|
||||
} else {
|
||||
userIdentifier = assertion.getSubject().getNameID().getValue();
|
||||
}
|
||||
|
||||
// Rest of your existing code...
|
||||
Optional<User> userOpt = userService.findByUsernameIgnoreCase(userIdentifier);
|
||||
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority("ROLE_USER");
|
||||
if (userOpt.isPresent()) {
|
||||
User user = userOpt.get();
|
||||
@@ -48,39 +91,27 @@ public class CustomSaml2ResponseAuthenticationConverter
|
||||
}
|
||||
}
|
||||
|
||||
// Extract the SessionIndexes
|
||||
List<String> sessionIndexes = new ArrayList<>();
|
||||
for (AuthnStatement authnStatement : assertion.getAuthnStatements()) {
|
||||
sessionIndexes.add(authnStatement.getSessionIndex());
|
||||
}
|
||||
|
||||
// Extract the Attributes
|
||||
Map<String, List<Object>> attributes = extractAttributes(assertion);
|
||||
|
||||
// Create the custom principal
|
||||
CustomSaml2AuthenticatedPrincipal principal =
|
||||
new CustomSaml2AuthenticatedPrincipal(nameId, attributes, nameId, sessionIndexes);
|
||||
new CustomSaml2AuthenticatedPrincipal(
|
||||
userIdentifier, attributes, userIdentifier, sessionIndexes);
|
||||
|
||||
// Create the Saml2Authentication
|
||||
return new Saml2Authentication(
|
||||
principal,
|
||||
responseToken.getToken().getSaml2Response(),
|
||||
Collections.singletonList(simpleGrantedAuthority));
|
||||
}
|
||||
|
||||
private Map<String, List<Object>> extractAttributes(Assertion assertion) {
|
||||
Map<String, List<Object>> attributes = new HashMap<>();
|
||||
for (AttributeStatement attributeStatement : assertion.getAttributeStatements()) {
|
||||
for (Attribute attribute : attributeStatement.getAttributes()) {
|
||||
String attributeName = attribute.getName();
|
||||
List<Object> values = new ArrayList<>();
|
||||
for (XMLObject xmlObject : attribute.getAttributeValues()) {
|
||||
log.info("BOOL: " + ((XSBoolean) xmlObject).getValue());
|
||||
values.add(((XSString) xmlObject).getValue());
|
||||
}
|
||||
attributes.put(attributeName, values);
|
||||
}
|
||||
}
|
||||
return attributes;
|
||||
private boolean hasAttribute(Map<String, List<Object>> attributes, String name) {
|
||||
return attributes.containsKey(name) && !attributes.get(name).isEmpty();
|
||||
}
|
||||
|
||||
private String getFirstAttributeValue(Map<String, List<Object>> attributes, String name) {
|
||||
List<Object> values = attributes.get(name);
|
||||
return values != null && !values.isEmpty() ? values.get(0).toString() : null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,8 +11,6 @@ import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||
import org.apache.pdfbox.pdmodel.PDPageContentStream.AppendMode;
|
||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
@@ -33,8 +31,6 @@ import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
@Tag(name = "General", description = "General APIs")
|
||||
public class CropController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(CropController.class);
|
||||
|
||||
private final CustomPDDocumentFactory pdfDocumentFactory;
|
||||
|
||||
private final PostHogService postHogService;
|
||||
|
||||
@@ -25,6 +25,7 @@ import org.springframework.web.servlet.mvc.support.RedirectAttributes;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -34,24 +35,28 @@ import stirling.software.SPDF.config.security.database.DatabaseBackupHelper;
|
||||
@Controller
|
||||
@RequestMapping("/api/v1/database")
|
||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||
@Tag(name = "Database", description = "Database APIs")
|
||||
@Tag(name = "Database", description = "Database APIs for backup, import, and management")
|
||||
public class DatabaseController {
|
||||
|
||||
@Autowired DatabaseBackupHelper databaseBackupHelper;
|
||||
|
||||
@Hidden
|
||||
@PostMapping(consumes = "multipart/form-data", value = "import-database")
|
||||
@Operation(
|
||||
summary = "Import database backup",
|
||||
description = "This endpoint imports a database backup from a SQL file.")
|
||||
summary = "Import a database backup file",
|
||||
description = "Uploads and imports a database backup SQL file.")
|
||||
@PostMapping(consumes = "multipart/form-data", value = "import-database")
|
||||
public String importDatabase(
|
||||
@RequestParam("fileInput") MultipartFile file, RedirectAttributes redirectAttributes)
|
||||
throws IllegalArgumentException, IOException {
|
||||
@Parameter(description = "SQL file to import", required = true)
|
||||
@RequestParam("fileInput")
|
||||
MultipartFile file,
|
||||
RedirectAttributes redirectAttributes)
|
||||
throws IOException {
|
||||
|
||||
if (file == null || file.isEmpty()) {
|
||||
redirectAttributes.addAttribute("error", "fileNullOrEmpty");
|
||||
return "redirect:/database";
|
||||
}
|
||||
log.info("Received file: {}", file.getOriginalFilename());
|
||||
|
||||
Path tempTemplatePath = Files.createTempFile("backup_", ".sql");
|
||||
try (InputStream in = file.getInputStream()) {
|
||||
Files.copy(in, tempTemplatePath, StandardCopyOption.REPLACE_EXISTING);
|
||||
@@ -69,9 +74,15 @@ public class DatabaseController {
|
||||
}
|
||||
|
||||
@Hidden
|
||||
@Operation(
|
||||
summary = "Import database backup by filename",
|
||||
description = "Imports a database backup file from the server using its file name.")
|
||||
@GetMapping("/import-database-file/{fileName}")
|
||||
public String importDatabaseFromBackupUI(@PathVariable String fileName)
|
||||
throws IllegalArgumentException, IOException {
|
||||
public String importDatabaseFromBackupUI(
|
||||
@Parameter(description = "Name of the file to import", required = true) @PathVariable
|
||||
String fileName)
|
||||
throws IOException {
|
||||
|
||||
if (fileName == null || fileName.isEmpty()) {
|
||||
return "redirect:/database?error=fileNullOrEmpty";
|
||||
}
|
||||
@@ -85,6 +96,7 @@ public class DatabaseController {
|
||||
return "redirect:/database?error=fileNotFound";
|
||||
}
|
||||
log.info("Received file: {}", fileName);
|
||||
|
||||
if (databaseBackupHelper.importDatabaseFromUI(fileName)) {
|
||||
log.info("File {} imported to database", fileName);
|
||||
return "redirect:/database?infoMessage=importIntoDatabaseSuccessed";
|
||||
@@ -93,12 +105,14 @@ public class DatabaseController {
|
||||
}
|
||||
|
||||
@Hidden
|
||||
@GetMapping("/delete/{fileName}")
|
||||
@Operation(
|
||||
summary = "Delete a database backup file",
|
||||
description =
|
||||
"This endpoint deletes a database backup file with the specified file name.")
|
||||
public String deleteFile(@PathVariable String fileName) {
|
||||
description = "Deletes a specified database backup file from the server.")
|
||||
@GetMapping("/delete/{fileName}")
|
||||
public String deleteFile(
|
||||
@Parameter(description = "Name of the file to delete", required = true) @PathVariable
|
||||
String fileName) {
|
||||
|
||||
if (fileName == null || fileName.isEmpty()) {
|
||||
throw new IllegalArgumentException("File must not be null or empty");
|
||||
}
|
||||
@@ -117,12 +131,13 @@ public class DatabaseController {
|
||||
}
|
||||
|
||||
@Hidden
|
||||
@GetMapping("/download/{fileName}")
|
||||
@Operation(
|
||||
summary = "Download a database backup file",
|
||||
description =
|
||||
"This endpoint downloads a database backup file with the specified file name.")
|
||||
public ResponseEntity<?> downloadFile(@PathVariable String fileName) {
|
||||
description = "Downloads the specified database backup file from the server.")
|
||||
@GetMapping("/download/{fileName}")
|
||||
public ResponseEntity<?> downloadFile(
|
||||
@Parameter(description = "Name of the file to download", required = true) @PathVariable
|
||||
String fileName) {
|
||||
if (fileName == null || fileName.isEmpty()) {
|
||||
throw new IllegalArgumentException("File must not be null or empty");
|
||||
}
|
||||
@@ -141,4 +156,22 @@ public class DatabaseController {
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(
|
||||
summary = "Create a database backup",
|
||||
description =
|
||||
"This endpoint triggers the creation of a database backup and redirects to the"
|
||||
+ " database management page.")
|
||||
@GetMapping("/createDatabaseBackup")
|
||||
public String createDatabaseBackup() {
|
||||
try {
|
||||
log.info("Starting database backup creation...");
|
||||
databaseBackupHelper.exportDatabase();
|
||||
log.info("Database backup successfully created.");
|
||||
} catch (IOException e) {
|
||||
log.error("Error creating database backup: {}", e.getMessage(), e);
|
||||
return "redirect:/database?error=" + e.getMessage();
|
||||
}
|
||||
return "redirect:/database?infoMessage=backupCreated";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,8 +20,6 @@ import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
|
||||
import org.apache.pdfbox.pdmodel.interactive.form.PDField;
|
||||
import org.apache.pdfbox.pdmodel.interactive.form.PDSignatureField;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
@@ -33,18 +31,18 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.SPDF.model.api.general.MergePdfsRequest;
|
||||
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
||||
import stirling.software.SPDF.utils.GeneralUtils;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@Slf4j
|
||||
@RequestMapping("/api/v1/general")
|
||||
@Tag(name = "General", description = "General APIs")
|
||||
public class MergeController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(MergeController.class);
|
||||
|
||||
private final CustomPDDocumentFactory pdfDocumentFactory;
|
||||
|
||||
@Autowired
|
||||
@@ -184,7 +182,7 @@ public class MergeController {
|
||||
baos.toByteArray(), mergedFileName); // Return the modified PDF
|
||||
|
||||
} catch (Exception ex) {
|
||||
logger.error("Error in merge pdf process", ex);
|
||||
log.error("Error in merge pdf process", ex);
|
||||
throw ex;
|
||||
} finally {
|
||||
for (File file : filesToDelete) {
|
||||
|
||||
@@ -12,8 +12,6 @@ import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
|
||||
import org.apache.pdfbox.util.Matrix;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
@@ -35,8 +33,6 @@ import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
@Tag(name = "General", description = "General APIs")
|
||||
public class MultiPageLayoutController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(MultiPageLayoutController.class);
|
||||
|
||||
private final CustomPDDocumentFactory pdfDocumentFactory;
|
||||
|
||||
@Autowired
|
||||
|
||||
@@ -8,8 +8,6 @@ import java.util.List;
|
||||
import org.apache.pdfbox.Loader;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
@@ -22,6 +20,7 @@ import io.github.pixee.security.Filenames;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.SPDF.model.SortTypes;
|
||||
import stirling.software.SPDF.model.api.PDFWithPageNums;
|
||||
import stirling.software.SPDF.model.api.general.RearrangePagesRequest;
|
||||
@@ -31,11 +30,10 @@ import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/general")
|
||||
@Slf4j
|
||||
@Tag(name = "General", description = "General APIs")
|
||||
public class RearrangePagesPDFController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(RearrangePagesPDFController.class);
|
||||
|
||||
private final CustomPDDocumentFactory pdfDocumentFactory;
|
||||
|
||||
@Autowired
|
||||
@@ -202,7 +200,7 @@ public class RearrangePagesPDFController {
|
||||
throw new IllegalArgumentException("Unsupported custom mode");
|
||||
}
|
||||
} catch (IllegalArgumentException e) {
|
||||
logger.error("Unsupported custom mode", e);
|
||||
log.error("Unsupported custom mode", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -230,8 +228,8 @@ public class RearrangePagesPDFController {
|
||||
} else {
|
||||
newPageOrder = GeneralUtils.parsePageList(pageOrderArr, totalPages, false);
|
||||
}
|
||||
logger.info("newPageOrder = " + newPageOrder);
|
||||
logger.info("totalPages = " + totalPages);
|
||||
log.info("newPageOrder = " + newPageOrder);
|
||||
log.info("totalPages = " + totalPages);
|
||||
// Create a new list to hold the pages in the new order
|
||||
List<PDPage> newPages = new ArrayList<>();
|
||||
for (int i = 0; i < newPageOrder.size(); i++) {
|
||||
@@ -254,7 +252,7 @@ public class RearrangePagesPDFController {
|
||||
.replaceFirst("[.][^.]+$", "")
|
||||
+ "_rearranged.pdf");
|
||||
} catch (IOException e) {
|
||||
logger.error("Failed rearranging documents", e);
|
||||
log.error("Failed rearranging documents", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,6 @@ import java.io.IOException;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.apache.pdfbox.pdmodel.PDPageTree;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
@@ -28,8 +26,6 @@ import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
@Tag(name = "General", description = "General APIs")
|
||||
public class RotationController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(RotationController.class);
|
||||
|
||||
private final CustomPDDocumentFactory pdfDocumentFactory;
|
||||
|
||||
@Autowired
|
||||
|
||||
@@ -13,8 +13,6 @@ import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
|
||||
import org.apache.pdfbox.util.Matrix;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
@@ -36,8 +34,6 @@ import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
@Tag(name = "General", description = "General APIs")
|
||||
public class ScalePagesController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ScalePagesController.class);
|
||||
|
||||
private final CustomPDDocumentFactory pdfDocumentFactory;
|
||||
|
||||
@Autowired
|
||||
|
||||
@@ -13,8 +13,6 @@ import java.util.zip.ZipOutputStream;
|
||||
import org.apache.pdfbox.Loader;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
@@ -28,16 +26,17 @@ import io.github.pixee.security.Filenames;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.SPDF.model.api.PDFWithPageNums;
|
||||
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/general")
|
||||
@Slf4j
|
||||
@Tag(name = "General", description = "General APIs")
|
||||
public class SplitPDFController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(SplitPDFController.class);
|
||||
private final CustomPDDocumentFactory pdfDocumentFactory;
|
||||
|
||||
@Autowired
|
||||
@@ -52,84 +51,114 @@ public class SplitPDFController {
|
||||
"This endpoint splits a given PDF file into separate documents based on the specified page numbers or ranges. Users can specify pages using individual numbers, ranges, or 'all' for every page. Input:PDF Output:PDF Type:SIMO")
|
||||
public ResponseEntity<byte[]> splitPdf(@ModelAttribute PDFWithPageNums request)
|
||||
throws IOException {
|
||||
MultipartFile file = request.getFileInput();
|
||||
String pages = request.getPageNumbers();
|
||||
// open the pdf document
|
||||
|
||||
PDDocument document = Loader.loadPDF(file.getBytes());
|
||||
// PdfMetadata metadata = PdfMetadataService.extractMetadataFromPdf(document);
|
||||
int totalPages = document.getNumberOfPages();
|
||||
List<Integer> pageNumbers = request.getPageNumbersList(document, false);
|
||||
if (!pageNumbers.contains(totalPages - 1)) {
|
||||
// Create a mutable ArrayList so we can add to it
|
||||
pageNumbers = new ArrayList<>(pageNumbers);
|
||||
pageNumbers.add(totalPages - 1);
|
||||
}
|
||||
|
||||
logger.info(
|
||||
"Splitting PDF into pages: {}",
|
||||
pageNumbers.stream().map(String::valueOf).collect(Collectors.joining(",")));
|
||||
|
||||
// split the document
|
||||
PDDocument document = null;
|
||||
Path zipFile = null;
|
||||
List<ByteArrayOutputStream> splitDocumentsBoas = new ArrayList<>();
|
||||
int previousPageNumber = 0;
|
||||
for (int splitPoint : pageNumbers) {
|
||||
try (PDDocument splitDocument =
|
||||
pdfDocumentFactory.createNewDocumentBasedOnOldDocument(document)) {
|
||||
for (int i = previousPageNumber; i <= splitPoint; i++) {
|
||||
PDPage page = document.getPage(i);
|
||||
splitDocument.addPage(page);
|
||||
logger.info("Adding page {} to split document", i);
|
||||
|
||||
try {
|
||||
|
||||
MultipartFile file = request.getFileInput();
|
||||
String pages = request.getPageNumbers();
|
||||
// open the pdf document
|
||||
|
||||
document = Loader.loadPDF(file.getBytes());
|
||||
// PdfMetadata metadata = PdfMetadataService.extractMetadataFromPdf(document);
|
||||
int totalPages = document.getNumberOfPages();
|
||||
List<Integer> pageNumbers = request.getPageNumbersList(document, false);
|
||||
if (!pageNumbers.contains(totalPages - 1)) {
|
||||
// Create a mutable ArrayList so we can add to it
|
||||
pageNumbers = new ArrayList<>(pageNumbers);
|
||||
pageNumbers.add(totalPages - 1);
|
||||
}
|
||||
|
||||
log.info(
|
||||
"Splitting PDF into pages: {}",
|
||||
pageNumbers.stream().map(String::valueOf).collect(Collectors.joining(",")));
|
||||
|
||||
// split the document
|
||||
splitDocumentsBoas = new ArrayList<>();
|
||||
int previousPageNumber = 0;
|
||||
for (int splitPoint : pageNumbers) {
|
||||
try (PDDocument splitDocument =
|
||||
pdfDocumentFactory.createNewDocumentBasedOnOldDocument(document)) {
|
||||
for (int i = previousPageNumber; i <= splitPoint; i++) {
|
||||
PDPage page = document.getPage(i);
|
||||
splitDocument.addPage(page);
|
||||
log.info("Adding page {} to split document", i);
|
||||
}
|
||||
previousPageNumber = splitPoint + 1;
|
||||
|
||||
// Transfer metadata to split pdf
|
||||
// PdfMetadataService.setMetadataToPdf(splitDocument, metadata);
|
||||
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
splitDocument.save(baos);
|
||||
|
||||
splitDocumentsBoas.add(baos);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed splitting documents and saving them", e);
|
||||
throw e;
|
||||
}
|
||||
previousPageNumber = splitPoint + 1;
|
||||
}
|
||||
|
||||
// Transfer metadata to split pdf
|
||||
// PdfMetadataService.setMetadataToPdf(splitDocument, metadata);
|
||||
// closing the original document
|
||||
document.close();
|
||||
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
splitDocument.save(baos);
|
||||
zipFile = Files.createTempFile("split_documents", ".zip");
|
||||
|
||||
splitDocumentsBoas.add(baos);
|
||||
String filename =
|
||||
Filenames.toSimpleFileName(file.getOriginalFilename())
|
||||
.replaceFirst("[.][^.]+$", "");
|
||||
try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(zipFile))) {
|
||||
// loop through the split documents and write them to the zip file
|
||||
for (int i = 0; i < splitDocumentsBoas.size(); i++) {
|
||||
String fileName = filename + "_" + (i + 1) + ".pdf";
|
||||
ByteArrayOutputStream baos = splitDocumentsBoas.get(i);
|
||||
byte[] pdf = baos.toByteArray();
|
||||
|
||||
// Add PDF file to the zip
|
||||
ZipEntry pdfEntry = new ZipEntry(fileName);
|
||||
zipOut.putNextEntry(pdfEntry);
|
||||
zipOut.write(pdf);
|
||||
zipOut.closeEntry();
|
||||
|
||||
log.info("Wrote split document {} to zip file", fileName);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed splitting documents and saving them", e);
|
||||
log.error("Failed writing to zip", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// closing the original document
|
||||
document.close();
|
||||
log.info("Successfully created zip file with split documents: {}", zipFile.toString());
|
||||
byte[] data = Files.readAllBytes(zipFile);
|
||||
Files.deleteIfExists(zipFile);
|
||||
|
||||
Path zipFile = Files.createTempFile("split_documents", ".zip");
|
||||
// return the Resource in the response
|
||||
return WebResponseUtils.bytesToWebResponse(
|
||||
data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM);
|
||||
|
||||
String filename =
|
||||
Filenames.toSimpleFileName(file.getOriginalFilename())
|
||||
.replaceFirst("[.][^.]+$", "");
|
||||
try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(zipFile))) {
|
||||
// loop through the split documents and write them to the zip file
|
||||
for (int i = 0; i < splitDocumentsBoas.size(); i++) {
|
||||
String fileName = filename + "_" + (i + 1) + ".pdf";
|
||||
ByteArrayOutputStream baos = splitDocumentsBoas.get(i);
|
||||
byte[] pdf = baos.toByteArray();
|
||||
} finally {
|
||||
try {
|
||||
// Close the main document
|
||||
if (document != null) {
|
||||
document.close();
|
||||
}
|
||||
|
||||
// Add PDF file to the zip
|
||||
ZipEntry pdfEntry = new ZipEntry(fileName);
|
||||
zipOut.putNextEntry(pdfEntry);
|
||||
zipOut.write(pdf);
|
||||
zipOut.closeEntry();
|
||||
// Close all ByteArrayOutputStreams
|
||||
for (ByteArrayOutputStream baos : splitDocumentsBoas) {
|
||||
if (baos != null) {
|
||||
baos.close();
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("Wrote split document {} to zip file", fileName);
|
||||
// Delete temporary zip file
|
||||
if (zipFile != null) {
|
||||
Files.deleteIfExists(zipFile);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Error while cleaning up resources", e);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed writing to zip", e);
|
||||
throw e;
|
||||
}
|
||||
|
||||
logger.info("Successfully created zip file with split documents: {}", zipFile.toString());
|
||||
byte[] data = Files.readAllBytes(zipFile);
|
||||
Files.deleteIfExists(zipFile);
|
||||
|
||||
// return the Resource in the response
|
||||
return WebResponseUtils.bytesToWebResponse(
|
||||
data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,8 +13,6 @@ import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDDocumentOutline;
|
||||
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDOutlineItem;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
@@ -32,6 +30,7 @@ import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.SPDF.model.PdfMetadata;
|
||||
import stirling.software.SPDF.model.api.SplitPdfByChaptersRequest;
|
||||
import stirling.software.SPDF.service.PdfMetadataService;
|
||||
@@ -39,12 +38,10 @@ import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/general")
|
||||
@Slf4j
|
||||
@Tag(name = "General", description = "General APIs")
|
||||
public class SplitPdfByChaptersController {
|
||||
|
||||
private static final Logger logger =
|
||||
LoggerFactory.getLogger(SplitPdfByChaptersController.class);
|
||||
|
||||
private final PdfMetadataService pdfMetadataService;
|
||||
|
||||
@Autowired
|
||||
@@ -59,70 +56,86 @@ public class SplitPdfByChaptersController {
|
||||
public ResponseEntity<byte[]> splitPdf(@ModelAttribute SplitPdfByChaptersRequest request)
|
||||
throws Exception {
|
||||
MultipartFile file = request.getFileInput();
|
||||
boolean includeMetadata = request.getIncludeMetadata();
|
||||
Integer bookmarkLevel =
|
||||
request.getBookmarkLevel(); // levels start from 0 (top most bookmarks)
|
||||
if (bookmarkLevel < 0) {
|
||||
return ResponseEntity.badRequest().body("Invalid bookmark level".getBytes());
|
||||
}
|
||||
PDDocument sourceDocument = Loader.loadPDF(file.getBytes());
|
||||
PDDocument sourceDocument = null;
|
||||
Path zipFile = null;
|
||||
|
||||
PDDocumentOutline outline = sourceDocument.getDocumentCatalog().getDocumentOutline();
|
||||
|
||||
if (outline == null) {
|
||||
logger.warn("No outline found for {}", file.getOriginalFilename());
|
||||
return ResponseEntity.badRequest().body("No outline found".getBytes());
|
||||
}
|
||||
List<Bookmark> bookmarks = new ArrayList<>();
|
||||
try {
|
||||
bookmarks =
|
||||
extractOutlineItems(
|
||||
sourceDocument,
|
||||
outline.getFirstChild(),
|
||||
bookmarks,
|
||||
outline.getFirstChild().getNextSibling(),
|
||||
0,
|
||||
bookmarkLevel);
|
||||
// to handle last page edge case
|
||||
bookmarks.get(bookmarks.size() - 1).setEndPage(sourceDocument.getNumberOfPages());
|
||||
Bookmark lastBookmark = bookmarks.get(bookmarks.size() - 1);
|
||||
boolean includeMetadata = request.getIncludeMetadata();
|
||||
Integer bookmarkLevel =
|
||||
request.getBookmarkLevel(); // levels start from 0 (top most bookmarks)
|
||||
if (bookmarkLevel < 0) {
|
||||
return ResponseEntity.badRequest().body("Invalid bookmark level".getBytes());
|
||||
}
|
||||
sourceDocument = Loader.loadPDF(file.getBytes());
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("Unable to extract outline items", e);
|
||||
return ResponseEntity.internalServerError()
|
||||
.body("Unable to extract outline items".getBytes());
|
||||
PDDocumentOutline outline = sourceDocument.getDocumentCatalog().getDocumentOutline();
|
||||
|
||||
if (outline == null) {
|
||||
log.warn("No outline found for {}", file.getOriginalFilename());
|
||||
return ResponseEntity.badRequest().body("No outline found".getBytes());
|
||||
}
|
||||
List<Bookmark> bookmarks = new ArrayList<>();
|
||||
try {
|
||||
bookmarks =
|
||||
extractOutlineItems(
|
||||
sourceDocument,
|
||||
outline.getFirstChild(),
|
||||
bookmarks,
|
||||
outline.getFirstChild().getNextSibling(),
|
||||
0,
|
||||
bookmarkLevel);
|
||||
// to handle last page edge case
|
||||
bookmarks.get(bookmarks.size() - 1).setEndPage(sourceDocument.getNumberOfPages());
|
||||
Bookmark lastBookmark = bookmarks.get(bookmarks.size() - 1);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Unable to extract outline items", e);
|
||||
return ResponseEntity.internalServerError()
|
||||
.body("Unable to extract outline items".getBytes());
|
||||
}
|
||||
|
||||
boolean allowDuplicates = request.getAllowDuplicates();
|
||||
if (!allowDuplicates) {
|
||||
/*
|
||||
duplicates are generated when multiple bookmarks correspond to the same page,
|
||||
if the user doesn't want duplicates mergeBookmarksThatCorrespondToSamePage() method will merge the titles of all
|
||||
the bookmarks that correspond to the same page, and treat them as a single bookmark
|
||||
*/
|
||||
bookmarks = mergeBookmarksThatCorrespondToSamePage(bookmarks);
|
||||
}
|
||||
for (Bookmark bookmark : bookmarks) {
|
||||
log.info(
|
||||
"{}::::{} to {}",
|
||||
bookmark.getTitle(),
|
||||
bookmark.getStartPage(),
|
||||
bookmark.getEndPage());
|
||||
}
|
||||
List<ByteArrayOutputStream> splitDocumentsBoas =
|
||||
getSplitDocumentsBoas(sourceDocument, bookmarks, includeMetadata);
|
||||
|
||||
zipFile = createZipFile(bookmarks, splitDocumentsBoas);
|
||||
|
||||
byte[] data = Files.readAllBytes(zipFile);
|
||||
Files.deleteIfExists(zipFile);
|
||||
|
||||
String filename =
|
||||
Filenames.toSimpleFileName(file.getOriginalFilename())
|
||||
.replaceFirst("[.][^.]+$", "");
|
||||
sourceDocument.close();
|
||||
return WebResponseUtils.bytesToWebResponse(
|
||||
data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM);
|
||||
} finally {
|
||||
try {
|
||||
if (sourceDocument != null) {
|
||||
sourceDocument.close();
|
||||
}
|
||||
if (zipFile != null) {
|
||||
Files.deleteIfExists(zipFile);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Error while cleaning up resources", e);
|
||||
}
|
||||
}
|
||||
|
||||
boolean allowDuplicates = request.getAllowDuplicates();
|
||||
if (!allowDuplicates) {
|
||||
/*
|
||||
duplicates are generated when multiple bookmarks correspond to the same page,
|
||||
if the user doesn't want duplicates mergeBookmarksThatCorrespondToSamePage() method will merge the titles of all
|
||||
the bookmarks that correspond to the same page, and treat them as a single bookmark
|
||||
*/
|
||||
bookmarks = mergeBookmarksThatCorrespondToSamePage(bookmarks);
|
||||
}
|
||||
for (Bookmark bookmark : bookmarks) {
|
||||
logger.info(
|
||||
"{}::::{} to {}",
|
||||
bookmark.getTitle(),
|
||||
bookmark.getStartPage(),
|
||||
bookmark.getEndPage());
|
||||
}
|
||||
List<ByteArrayOutputStream> splitDocumentsBoas =
|
||||
getSplitDocumentsBoas(sourceDocument, bookmarks, includeMetadata);
|
||||
|
||||
Path zipFile = createZipFile(bookmarks, splitDocumentsBoas);
|
||||
|
||||
byte[] data = Files.readAllBytes(zipFile);
|
||||
Files.deleteIfExists(zipFile);
|
||||
|
||||
String filename =
|
||||
Filenames.toSimpleFileName(file.getOriginalFilename())
|
||||
.replaceFirst("[.][^.]+$", "");
|
||||
sourceDocument.close();
|
||||
return WebResponseUtils.bytesToWebResponse(
|
||||
data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM);
|
||||
}
|
||||
|
||||
private List<Bookmark> mergeBookmarksThatCorrespondToSamePage(List<Bookmark> bookmarks) {
|
||||
@@ -240,14 +253,14 @@ public class SplitPdfByChaptersController {
|
||||
zipOut.write(pdf);
|
||||
zipOut.closeEntry();
|
||||
|
||||
logger.info("Wrote split document {} to zip file", fileName);
|
||||
log.info("Wrote split document {} to zip file", fileName);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed writing to zip", e);
|
||||
log.error("Failed writing to zip", e);
|
||||
throw e;
|
||||
}
|
||||
|
||||
logger.info("Successfully created zip file with split documents: {}", zipFile);
|
||||
log.info("Successfully created zip file with split documents: {}", zipFile);
|
||||
return zipFile;
|
||||
}
|
||||
|
||||
@@ -268,7 +281,7 @@ public class SplitPdfByChaptersController {
|
||||
i++) {
|
||||
PDPage page = sourceDocument.getPage(i);
|
||||
splitDocument.addPage(page);
|
||||
logger.info("Adding page {} to split document", i);
|
||||
log.info("Adding page {} to split document", i);
|
||||
}
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
if (includeMetadata) {
|
||||
@@ -279,7 +292,7 @@ public class SplitPdfByChaptersController {
|
||||
|
||||
splitDocumentsBoas.add(baos);
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed splitting documents and saving them", e);
|
||||
log.error("Failed splitting documents and saving them", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,8 +18,6 @@ import org.apache.pdfbox.pdmodel.PDPageContentStream.AppendMode;
|
||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
|
||||
import org.apache.pdfbox.util.Matrix;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
@@ -42,9 +40,6 @@ import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
@Tag(name = "General", description = "General APIs")
|
||||
public class SplitPdfBySectionsController {
|
||||
|
||||
private static final Logger logger =
|
||||
LoggerFactory.getLogger(SplitPdfBySectionsController.class);
|
||||
|
||||
private final CustomPDDocumentFactory pdfDocumentFactory;
|
||||
|
||||
@Autowired
|
||||
@@ -105,15 +100,13 @@ public class SplitPdfBySectionsController {
|
||||
|
||||
if (sectionNum == horiz * verti) pageNum++;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("exception", e);
|
||||
} finally {
|
||||
data = Files.readAllBytes(zipFile);
|
||||
return WebResponseUtils.bytesToWebResponse(
|
||||
data, filename + "_split.zip", MediaType.APPLICATION_OCTET_STREAM);
|
||||
|
||||
} finally {
|
||||
Files.deleteIfExists(zipFile);
|
||||
}
|
||||
|
||||
return WebResponseUtils.bytesToWebResponse(
|
||||
data, filename + "_split.zip", MediaType.APPLICATION_OCTET_STREAM);
|
||||
}
|
||||
|
||||
public List<PDDocument> splitPdfPages(
|
||||
|
||||
@@ -10,8 +10,6 @@ import java.util.zip.ZipOutputStream;
|
||||
import org.apache.pdfbox.Loader;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
@@ -25,6 +23,7 @@ import io.github.pixee.security.Filenames;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.SPDF.model.api.general.SplitPdfBySizeOrCountRequest;
|
||||
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
||||
import stirling.software.SPDF.utils.GeneralUtils;
|
||||
@@ -32,10 +31,10 @@ import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/general")
|
||||
@Slf4j
|
||||
@Tag(name = "General", description = "General APIs")
|
||||
public class SplitPdfBySizeController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(SplitPdfBySizeController.class);
|
||||
private final CustomPDDocumentFactory pdfDocumentFactory;
|
||||
|
||||
@Autowired
|
||||
@@ -78,7 +77,7 @@ public class SplitPdfBySizeController {
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("exception", e);
|
||||
log.error("exception", e);
|
||||
} finally {
|
||||
data = Files.readAllBytes(zipFile);
|
||||
Files.deleteIfExists(zipFile);
|
||||
|
||||
@@ -11,8 +11,6 @@ import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
@@ -32,8 +30,6 @@ import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
@Tag(name = "General", description = "General APIs")
|
||||
public class ToSinglePageController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ToSinglePageController.class);
|
||||
|
||||
private final CustomPDDocumentFactory pdfDocumentFactory;
|
||||
|
||||
@Autowired
|
||||
|
||||
@@ -244,8 +244,8 @@ public class UserController {
|
||||
return new RedirectView("/addUsers?messageType=invalidRole", true);
|
||||
}
|
||||
|
||||
if (authType.equalsIgnoreCase(AuthenticationType.OAUTH2.toString())) {
|
||||
userService.saveUser(username, AuthenticationType.OAUTH2, role);
|
||||
if (authType.equalsIgnoreCase(AuthenticationType.SSO.toString())) {
|
||||
userService.saveUser(username, AuthenticationType.SSO, role);
|
||||
} else {
|
||||
if (password.isBlank()) {
|
||||
return new RedirectView("/addUsers?messageType=invalidPassword", true);
|
||||
|
||||
@@ -14,8 +14,6 @@ import java.util.zip.ZipOutputStream;
|
||||
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.pdfbox.rendering.ImageType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
@@ -29,6 +27,7 @@ import io.github.pixee.security.Filenames;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.SPDF.model.api.converters.ConvertToImageRequest;
|
||||
import stirling.software.SPDF.model.api.converters.ConvertToPdfRequest;
|
||||
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
||||
@@ -40,11 +39,10 @@ import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/convert")
|
||||
@Slf4j
|
||||
@Tag(name = "Convert", description = "Convert APIs")
|
||||
public class ConvertImgPDFController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ConvertImgPDFController.class);
|
||||
|
||||
private final CustomPDDocumentFactory pdfDocumentFactory;
|
||||
|
||||
@Autowired
|
||||
@@ -65,112 +63,137 @@ public class ConvertImgPDFController {
|
||||
String colorType = request.getColorType();
|
||||
String dpi = request.getDpi();
|
||||
|
||||
byte[] pdfBytes = file.getBytes();
|
||||
ImageType colorTypeResult = ImageType.RGB;
|
||||
if ("greyscale".equals(colorType)) {
|
||||
colorTypeResult = ImageType.GRAY;
|
||||
} else if ("blackwhite".equals(colorType)) {
|
||||
colorTypeResult = ImageType.BINARY;
|
||||
}
|
||||
// returns bytes for image
|
||||
boolean singleImage = "single".equals(singleOrMultiple);
|
||||
Path tempFile = null;
|
||||
Path tempOutputDir = null;
|
||||
Path tempPdfPath = null;
|
||||
byte[] result = null;
|
||||
String filename =
|
||||
Filenames.toSimpleFileName(file.getOriginalFilename())
|
||||
.replaceFirst("[.][^.]+$", "");
|
||||
|
||||
result =
|
||||
PdfUtils.convertFromPdf(
|
||||
pdfBytes,
|
||||
"webp".equalsIgnoreCase(imageFormat) ? "png" : imageFormat.toUpperCase(),
|
||||
colorTypeResult,
|
||||
singleImage,
|
||||
Integer.valueOf(dpi),
|
||||
filename);
|
||||
if (result == null || result.length == 0) {
|
||||
logger.error("resultant bytes for {} is null, error converting ", filename);
|
||||
}
|
||||
if ("webp".equalsIgnoreCase(imageFormat) && !CheckProgramInstall.isPythonAvailable()) {
|
||||
throw new IOException("Python is not installed. Required for WebP conversion.");
|
||||
} else if ("webp".equalsIgnoreCase(imageFormat)
|
||||
&& CheckProgramInstall.isPythonAvailable()) {
|
||||
// Write the output stream to a temp file
|
||||
Path tempFile = Files.createTempFile("temp_png", ".png");
|
||||
try (FileOutputStream fos = new FileOutputStream(tempFile.toFile())) {
|
||||
fos.write(result);
|
||||
fos.flush();
|
||||
try {
|
||||
byte[] pdfBytes = file.getBytes();
|
||||
ImageType colorTypeResult = ImageType.RGB;
|
||||
if ("greyscale".equals(colorType)) {
|
||||
colorTypeResult = ImageType.GRAY;
|
||||
} else if ("blackwhite".equals(colorType)) {
|
||||
colorTypeResult = ImageType.BINARY;
|
||||
}
|
||||
// returns bytes for image
|
||||
boolean singleImage = "single".equals(singleOrMultiple);
|
||||
String filename =
|
||||
Filenames.toSimpleFileName(file.getOriginalFilename())
|
||||
.replaceFirst("[.][^.]+$", "");
|
||||
|
||||
String pythonVersion = CheckProgramInstall.getAvailablePythonCommand();
|
||||
|
||||
List<String> command = new ArrayList<>();
|
||||
command.add(pythonVersion);
|
||||
command.add("./scripts/png_to_webp.py"); // Python script to handle the conversion
|
||||
|
||||
// Create a temporary directory for the output WebP files
|
||||
Path tempOutputDir = Files.createTempDirectory("webp_output");
|
||||
if (singleImage) {
|
||||
// Run the Python script to convert PNG to WebP
|
||||
command.add(tempFile.toString());
|
||||
command.add(tempOutputDir.toString());
|
||||
command.add("--single");
|
||||
} else {
|
||||
// Save the uploaded PDF to a temporary file
|
||||
Path tempPdfPath = Files.createTempFile("temp_pdf", ".pdf");
|
||||
file.transferTo(tempPdfPath.toFile());
|
||||
// Run the Python script to convert PDF to WebP
|
||||
command.add(tempPdfPath.toString());
|
||||
command.add(tempOutputDir.toString());
|
||||
result =
|
||||
PdfUtils.convertFromPdf(
|
||||
pdfBytes,
|
||||
"webp".equalsIgnoreCase(imageFormat)
|
||||
? "png"
|
||||
: imageFormat.toUpperCase(),
|
||||
colorTypeResult,
|
||||
singleImage,
|
||||
Integer.valueOf(dpi),
|
||||
filename);
|
||||
if (result == null || result.length == 0) {
|
||||
log.error("resultant bytes for {} is null, error converting ", filename);
|
||||
}
|
||||
command.add("--dpi");
|
||||
command.add(dpi);
|
||||
ProcessExecutorResult resultProcess =
|
||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.PYTHON_OPENCV)
|
||||
.runCommandWithOutputHandling(command);
|
||||
|
||||
// Find all WebP files in the output directory
|
||||
List<Path> webpFiles =
|
||||
Files.walk(tempOutputDir)
|
||||
.filter(path -> path.toString().endsWith(".webp"))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (webpFiles.isEmpty()) {
|
||||
logger.error("No WebP files were created in: {}", tempOutputDir.toString());
|
||||
throw new IOException("No WebP files were created. " + resultProcess.getMessages());
|
||||
}
|
||||
|
||||
byte[] bodyBytes = new byte[0];
|
||||
|
||||
if (webpFiles.size() == 1) {
|
||||
// Return the single WebP file directly
|
||||
Path webpFilePath = webpFiles.get(0);
|
||||
bodyBytes = Files.readAllBytes(webpFilePath);
|
||||
} else {
|
||||
// Create a ZIP file containing all WebP images
|
||||
ByteArrayOutputStream zipOutputStream = new ByteArrayOutputStream();
|
||||
try (ZipOutputStream zos = new ZipOutputStream(zipOutputStream)) {
|
||||
for (Path webpFile : webpFiles) {
|
||||
zos.putNextEntry(new ZipEntry(webpFile.getFileName().toString()));
|
||||
Files.copy(webpFile, zos);
|
||||
zos.closeEntry();
|
||||
}
|
||||
if ("webp".equalsIgnoreCase(imageFormat) && !CheckProgramInstall.isPythonAvailable()) {
|
||||
throw new IOException("Python is not installed. Required for WebP conversion.");
|
||||
} else if ("webp".equalsIgnoreCase(imageFormat)
|
||||
&& CheckProgramInstall.isPythonAvailable()) {
|
||||
// Write the output stream to a temp file
|
||||
tempFile = Files.createTempFile("temp_png", ".png");
|
||||
try (FileOutputStream fos = new FileOutputStream(tempFile.toFile())) {
|
||||
fos.write(result);
|
||||
fos.flush();
|
||||
}
|
||||
bodyBytes = zipOutputStream.toByteArray();
|
||||
}
|
||||
// Clean up the temporary files
|
||||
Files.deleteIfExists(tempFile);
|
||||
if (tempOutputDir != null) FileUtils.deleteDirectory(tempOutputDir.toFile());
|
||||
result = bodyBytes;
|
||||
}
|
||||
|
||||
if (singleImage) {
|
||||
String docName = filename + "." + imageFormat;
|
||||
MediaType mediaType = MediaType.parseMediaType(getMediaType(imageFormat));
|
||||
return WebResponseUtils.bytesToWebResponse(result, docName, mediaType);
|
||||
} else {
|
||||
String zipFilename = filename + "_convertedToImages.zip";
|
||||
return WebResponseUtils.bytesToWebResponse(
|
||||
result, zipFilename, MediaType.APPLICATION_OCTET_STREAM);
|
||||
String pythonVersion = CheckProgramInstall.getAvailablePythonCommand();
|
||||
|
||||
List<String> command = new ArrayList<>();
|
||||
command.add(pythonVersion);
|
||||
command.add("./scripts/png_to_webp.py"); // Python script to handle the conversion
|
||||
|
||||
// Create a temporary directory for the output WebP files
|
||||
tempOutputDir = Files.createTempDirectory("webp_output");
|
||||
if (singleImage) {
|
||||
// Run the Python script to convert PNG to WebP
|
||||
command.add(tempFile.toString());
|
||||
command.add(tempOutputDir.toString());
|
||||
command.add("--single");
|
||||
} else {
|
||||
// Save the uploaded PDF to a temporary file
|
||||
tempPdfPath = Files.createTempFile("temp_pdf", ".pdf");
|
||||
file.transferTo(tempPdfPath.toFile());
|
||||
// Run the Python script to convert PDF to WebP
|
||||
command.add(tempPdfPath.toString());
|
||||
command.add(tempOutputDir.toString());
|
||||
}
|
||||
command.add("--dpi");
|
||||
command.add(dpi);
|
||||
ProcessExecutorResult resultProcess =
|
||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.PYTHON_OPENCV)
|
||||
.runCommandWithOutputHandling(command);
|
||||
|
||||
// Find all WebP files in the output directory
|
||||
List<Path> webpFiles =
|
||||
Files.walk(tempOutputDir)
|
||||
.filter(path -> path.toString().endsWith(".webp"))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (webpFiles.isEmpty()) {
|
||||
log.error("No WebP files were created in: {}", tempOutputDir.toString());
|
||||
throw new IOException(
|
||||
"No WebP files were created. " + resultProcess.getMessages());
|
||||
}
|
||||
|
||||
byte[] bodyBytes = new byte[0];
|
||||
|
||||
if (webpFiles.size() == 1) {
|
||||
// Return the single WebP file directly
|
||||
Path webpFilePath = webpFiles.get(0);
|
||||
bodyBytes = Files.readAllBytes(webpFilePath);
|
||||
} else {
|
||||
// Create a ZIP file containing all WebP images
|
||||
ByteArrayOutputStream zipOutputStream = new ByteArrayOutputStream();
|
||||
try (ZipOutputStream zos = new ZipOutputStream(zipOutputStream)) {
|
||||
for (Path webpFile : webpFiles) {
|
||||
zos.putNextEntry(new ZipEntry(webpFile.getFileName().toString()));
|
||||
Files.copy(webpFile, zos);
|
||||
zos.closeEntry();
|
||||
}
|
||||
}
|
||||
bodyBytes = zipOutputStream.toByteArray();
|
||||
}
|
||||
// Clean up the temporary files
|
||||
Files.deleteIfExists(tempFile);
|
||||
if (tempOutputDir != null) FileUtils.deleteDirectory(tempOutputDir.toFile());
|
||||
result = bodyBytes;
|
||||
}
|
||||
|
||||
if (singleImage) {
|
||||
String docName = filename + "." + imageFormat;
|
||||
MediaType mediaType = MediaType.parseMediaType(getMediaType(imageFormat));
|
||||
return WebResponseUtils.bytesToWebResponse(result, docName, mediaType);
|
||||
} else {
|
||||
String zipFilename = filename + "_convertedToImages.zip";
|
||||
return WebResponseUtils.bytesToWebResponse(
|
||||
result, zipFilename, MediaType.APPLICATION_OCTET_STREAM);
|
||||
}
|
||||
|
||||
} finally {
|
||||
try {
|
||||
// Clean up temporary files
|
||||
if (tempFile != null) {
|
||||
Files.deleteIfExists(tempFile);
|
||||
}
|
||||
if (tempPdfPath != null) {
|
||||
Files.deleteIfExists(tempPdfPath);
|
||||
}
|
||||
if (tempOutputDir != null) {
|
||||
FileUtils.deleteDirectory(tempOutputDir.toFile());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Error cleaning up temporary files", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +208,13 @@ public class ConvertImgPDFController {
|
||||
String fitOption = request.getFitOption();
|
||||
String colorType = request.getColorType();
|
||||
boolean autoRotate = request.isAutoRotate();
|
||||
|
||||
// Handle Null entries for formdata
|
||||
if (colorType == null || colorType.isBlank()) {
|
||||
colorType = "color";
|
||||
}
|
||||
if (fitOption == null || fitOption.isEmpty()) {
|
||||
fitOption = "fillPage";
|
||||
}
|
||||
// Convert the file to PDF and get the resulting bytes
|
||||
byte[] bytes =
|
||||
PdfUtils.imageToPdf(file, fitOption, autoRotate, colorType, pdfDocumentFactory);
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
package stirling.software.SPDF.controller.api.converters;
|
||||
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.File;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
@@ -21,6 +20,7 @@ import io.github.pixee.security.Filenames;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.SPDF.model.api.converters.PdfToPdfARequest;
|
||||
import stirling.software.SPDF.utils.ProcessExecutor;
|
||||
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
|
||||
@@ -28,68 +28,98 @@ import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/convert")
|
||||
@Slf4j
|
||||
@Tag(name = "Convert", description = "Convert APIs")
|
||||
public class ConvertPDFToPDFA {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ConvertPDFToPDFA.class);
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/pdf/pdfa")
|
||||
@Operation(
|
||||
summary = "Convert a PDF to a PDF/A",
|
||||
description =
|
||||
"This endpoint converts a PDF file to a PDF/A file. PDF/A is a format designed for long-term archiving of digital documents. Input:PDF Output:PDF Type:SISO")
|
||||
"This endpoint converts a PDF file to a PDF/A file using LibreOffice. PDF/A is a format designed for long-term archiving of digital documents. Input:PDF Output:PDF Type:SISO")
|
||||
public ResponseEntity<byte[]> pdfToPdfA(@ModelAttribute PdfToPdfARequest request)
|
||||
throws Exception {
|
||||
MultipartFile inputFile = request.getFileInput();
|
||||
String outputFormat = request.getOutputFormat();
|
||||
|
||||
// Convert MultipartFile to byte[]
|
||||
byte[] pdfBytes = inputFile.getBytes();
|
||||
|
||||
// Save the uploaded file to a temporary location
|
||||
Path tempInputFile = Files.createTempFile("input_", ".pdf");
|
||||
try (OutputStream outputStream = new FileOutputStream(tempInputFile.toFile())) {
|
||||
outputStream.write(pdfBytes);
|
||||
// Validate input file type
|
||||
if (!"application/pdf".equals(inputFile.getContentType())) {
|
||||
log.error("Invalid input file type: {}", inputFile.getContentType());
|
||||
throw new IllegalArgumentException("Input file must be a PDF");
|
||||
}
|
||||
|
||||
// Prepare the output file path
|
||||
Path tempOutputFile = Files.createTempFile("output_", ".pdf");
|
||||
|
||||
// Prepare the ghostscript command
|
||||
List<String> command = new ArrayList<>();
|
||||
command.add("gs");
|
||||
command.add("-dPDFA=" + ("pdfa".equals(outputFormat) ? "2" : "1"));
|
||||
command.add("-dNOPAUSE");
|
||||
command.add("-dBATCH");
|
||||
command.add("-sColorConversionStrategy=sRGB");
|
||||
command.add("-sDEVICE=pdfwrite");
|
||||
command.add("-dPDFACompatibilityPolicy=2");
|
||||
command.add("-o");
|
||||
command.add(tempOutputFile.toString());
|
||||
command.add(tempInputFile.toString());
|
||||
|
||||
ProcessExecutorResult returnCode =
|
||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT)
|
||||
.runCommandWithOutputHandling(command);
|
||||
|
||||
if (returnCode.getRc() != 0) {
|
||||
logger.info(
|
||||
outputFormat + " conversion failed with return code: " + returnCode.getRc());
|
||||
// Get the original filename without extension
|
||||
String originalFileName = Filenames.toSimpleFileName(inputFile.getOriginalFilename());
|
||||
if (originalFileName == null || originalFileName.trim().isEmpty()) {
|
||||
originalFileName = "output.pdf";
|
||||
}
|
||||
String baseFileName =
|
||||
originalFileName.contains(".")
|
||||
? originalFileName.substring(0, originalFileName.lastIndexOf('.'))
|
||||
: originalFileName;
|
||||
|
||||
Path tempInputFile = null;
|
||||
Path tempOutputDir = null;
|
||||
byte[] fileBytes;
|
||||
|
||||
try {
|
||||
byte[] pdfBytesOutput = Files.readAllBytes(tempOutputFile);
|
||||
// Return the optimized PDF as a response
|
||||
String outputFilename =
|
||||
Filenames.toSimpleFileName(inputFile.getOriginalFilename())
|
||||
.replaceFirst("[.][^.]+$", "")
|
||||
+ "_PDFA.pdf";
|
||||
// Save uploaded file to temp location
|
||||
tempInputFile = Files.createTempFile("input_", ".pdf");
|
||||
inputFile.transferTo(tempInputFile);
|
||||
|
||||
// Create temp output directory
|
||||
tempOutputDir = Files.createTempDirectory("output_");
|
||||
|
||||
// Determine PDF/A filter based on requested format
|
||||
String pdfFilter =
|
||||
"pdfa".equals(outputFormat)
|
||||
? "writer_pdf_Export:{'SelectPdfVersion':{'Value':'2'}}:writer_pdf_Export"
|
||||
: "writer_pdf_Export:{'SelectPdfVersion':{'Value':'1'}}:writer_pdf_Export";
|
||||
|
||||
// Prepare LibreOffice command
|
||||
List<String> command =
|
||||
new ArrayList<>(
|
||||
Arrays.asList(
|
||||
"soffice",
|
||||
"--headless",
|
||||
"--nologo",
|
||||
"--convert-to",
|
||||
"pdf:" + pdfFilter,
|
||||
"--outdir",
|
||||
tempOutputDir.toString(),
|
||||
tempInputFile.toString()));
|
||||
|
||||
ProcessExecutorResult returnCode =
|
||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE)
|
||||
.runCommandWithOutputHandling(command);
|
||||
|
||||
if (returnCode.getRc() != 0) {
|
||||
log.error("PDF/A conversion failed with return code: {}", returnCode.getRc());
|
||||
throw new RuntimeException("PDF/A conversion failed");
|
||||
}
|
||||
|
||||
// Get the output file
|
||||
File[] outputFiles = tempOutputDir.toFile().listFiles();
|
||||
if (outputFiles == null || outputFiles.length != 1) {
|
||||
throw new RuntimeException(
|
||||
"Expected exactly one output file but found "
|
||||
+ (outputFiles == null ? "none" : outputFiles.length));
|
||||
}
|
||||
|
||||
fileBytes = FileUtils.readFileToByteArray(outputFiles[0]);
|
||||
String outputFilename = baseFileName + "_PDFA.pdf";
|
||||
|
||||
return WebResponseUtils.bytesToWebResponse(
|
||||
pdfBytesOutput, outputFilename, MediaType.APPLICATION_PDF);
|
||||
fileBytes, outputFilename, MediaType.APPLICATION_PDF);
|
||||
|
||||
} finally {
|
||||
// Clean up the temporary files
|
||||
Files.deleteIfExists(tempInputFile);
|
||||
Files.deleteIfExists(tempOutputFile);
|
||||
// Clean up temporary files
|
||||
if (tempInputFile != null) {
|
||||
Files.deleteIfExists(tempInputFile);
|
||||
}
|
||||
if (tempOutputDir != null) {
|
||||
FileUtils.deleteDirectory(tempOutputDir.toFile());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,6 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
@@ -19,6 +17,7 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.SPDF.model.api.converters.UrlToPdfRequest;
|
||||
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
||||
import stirling.software.SPDF.utils.GeneralUtils;
|
||||
@@ -28,11 +27,10 @@ import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@Tag(name = "Convert", description = "Convert APIs")
|
||||
@Slf4j
|
||||
@RequestMapping("/api/v1/convert")
|
||||
public class ConvertWebsiteToPDF {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ConvertWebsiteToPDF.class);
|
||||
|
||||
private final CustomPDDocumentFactory pdfDocumentFactory;
|
||||
|
||||
@Autowired
|
||||
@@ -88,7 +86,7 @@ public class ConvertWebsiteToPDF {
|
||||
try {
|
||||
Files.deleteIfExists(tempOutputFile);
|
||||
} catch (IOException e) {
|
||||
logger.error("Error deleting temporary output file", e);
|
||||
log.error("Error deleting temporary output file", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
package stirling.software.SPDF.controller.api.converters;
|
||||
|
||||
import java.io.StringWriter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.commons.csv.CSVFormat;
|
||||
import org.apache.commons.csv.QuoteMode;
|
||||
import org.apache.pdfbox.Loader;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.ContentDisposition;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
@@ -18,79 +16,38 @@ import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import com.opencsv.CSVWriter;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import stirling.software.SPDF.controller.api.CropController;
|
||||
import stirling.software.SPDF.controller.api.strippers.PDFTableStripper;
|
||||
import stirling.software.SPDF.model.api.extract.PDFFilePage;
|
||||
import stirling.software.SPDF.pdf.FlexibleCSVWriter;
|
||||
import technology.tabula.ObjectExtractor;
|
||||
import technology.tabula.Page;
|
||||
import technology.tabula.Table;
|
||||
import technology.tabula.extractors.SpreadsheetExtractionAlgorithm;
|
||||
import technology.tabula.writers.Writer;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/convert")
|
||||
@Tag(name = "Convert", description = "Convert APIs")
|
||||
public class ExtractCSVController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(CropController.class);
|
||||
|
||||
@PostMapping(value = "/pdf/csv", consumes = "multipart/form-data")
|
||||
@Operation(
|
||||
summary = "Extracts a CSV document from a PDF",
|
||||
description =
|
||||
"This operation takes an input PDF file and returns CSV file of whole page. Input:PDF Output:CSV Type:SISO")
|
||||
public ResponseEntity<String> PdfToCsv(@ModelAttribute PDFFilePage form) throws Exception {
|
||||
|
||||
ArrayList<String> tableData = new ArrayList<>();
|
||||
int columnsCount = 0;
|
||||
|
||||
try (PDDocument document = Loader.loadPDF(form.getFileInput().getBytes())) {
|
||||
final double res = 72; // PDF units are at 72 DPI
|
||||
PDFTableStripper stripper = new PDFTableStripper();
|
||||
PDPage pdPage = document.getPage(form.getPageId() - 1);
|
||||
stripper.extractTable(pdPage);
|
||||
columnsCount = stripper.getColumns();
|
||||
for (int c = 0; c < columnsCount; ++c) {
|
||||
for (int r = 0; r < stripper.getRows(); ++r) {
|
||||
tableData.add(stripper.getText(r, c));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ArrayList<String> notEmptyColumns = new ArrayList<>();
|
||||
|
||||
for (String item : tableData) {
|
||||
if (!item.trim().isEmpty()) {
|
||||
notEmptyColumns.add(item);
|
||||
} else {
|
||||
columnsCount--;
|
||||
}
|
||||
}
|
||||
|
||||
List<String> fullTable =
|
||||
notEmptyColumns.stream()
|
||||
.map(
|
||||
(entity) ->
|
||||
entity.replace('\n', ' ')
|
||||
.replace('\r', ' ')
|
||||
.trim()
|
||||
.replaceAll("\\s{2,}", "|"))
|
||||
.toList();
|
||||
|
||||
int rowsCount = fullTable.get(0).split("\\|").length;
|
||||
|
||||
ArrayList<String> headersList = getTableHeaders(columnsCount, fullTable);
|
||||
ArrayList<String> recordList = getRecordsList(rowsCount, fullTable);
|
||||
|
||||
if (headersList.size() == 0 && recordList.size() == 0) {
|
||||
throw new Exception("No table detected, no headers or records found");
|
||||
}
|
||||
|
||||
StringWriter writer = new StringWriter();
|
||||
try (CSVWriter csvWriter = new CSVWriter(writer)) {
|
||||
csvWriter.writeNext(headersList.toArray(new String[0]));
|
||||
for (String record : recordList) {
|
||||
csvWriter.writeNext(record.split("\\|"));
|
||||
try (PDDocument document = Loader.loadPDF(form.getFileInput().getBytes())) {
|
||||
CSVFormat format =
|
||||
CSVFormat.EXCEL.builder().setEscape('"').setQuoteMode(QuoteMode.ALL).build();
|
||||
Writer csvWriter = new FlexibleCSVWriter(format);
|
||||
SpreadsheetExtractionAlgorithm sea = new SpreadsheetExtractionAlgorithm();
|
||||
try (ObjectExtractor extractor = new ObjectExtractor(document)) {
|
||||
Page page = extractor.extract(form.getPageId());
|
||||
List<Table> tables = sea.extract(page);
|
||||
csvWriter.write(writer, tables);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,33 +64,4 @@ public class ExtractCSVController {
|
||||
|
||||
return ResponseEntity.ok().headers(headers).body(writer.toString());
|
||||
}
|
||||
|
||||
private ArrayList<String> getRecordsList(int rowsCounts, List<String> items) {
|
||||
ArrayList<String> recordsList = new ArrayList<>();
|
||||
|
||||
for (int b = 1; b < rowsCounts; b++) {
|
||||
StringBuilder strbldr = new StringBuilder();
|
||||
|
||||
for (int i = 0; i < items.size(); i++) {
|
||||
String[] parts = items.get(i).split("\\|");
|
||||
strbldr.append(parts[b]);
|
||||
if (i != items.size() - 1) {
|
||||
strbldr.append("|");
|
||||
}
|
||||
}
|
||||
recordsList.add(strbldr.toString());
|
||||
}
|
||||
|
||||
return recordsList;
|
||||
}
|
||||
|
||||
private ArrayList<String> getTableHeaders(int columnsCount, List<String> items) {
|
||||
ArrayList<String> resultList = new ArrayList<>();
|
||||
for (int i = 0; i < columnsCount; i++) {
|
||||
String[] parts = items.get(i).split("\\|");
|
||||
resultList.add(parts[0]);
|
||||
}
|
||||
|
||||
return resultList;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,6 @@ import org.apache.pdfbox.Loader;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.text.PDFTextStripper;
|
||||
import org.apache.pdfbox.text.TextPosition;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
@@ -22,16 +20,16 @@ import io.github.pixee.security.Filenames;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.SPDF.model.api.misc.ExtractHeaderRequest;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/misc")
|
||||
@Slf4j
|
||||
@Tag(name = "Misc", description = "Miscellaneous APIs")
|
||||
public class AutoRenameController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(AutoRenameController.class);
|
||||
|
||||
private static final float TITLE_FONT_SIZE_THRESHOLD = 20.0f;
|
||||
private static final int LINE_LIMIT = 200;
|
||||
|
||||
@@ -133,7 +131,7 @@ public class AutoRenameController {
|
||||
header = header.replaceAll("[/\\\\?%*:|\"<>]", "").trim();
|
||||
return WebResponseUtils.pdfDocToWebResponse(document, header + ".pdf");
|
||||
} else {
|
||||
logger.info("File has no good title to be found");
|
||||
log.info("File has no good title to be found");
|
||||
return WebResponseUtils.pdfDocToWebResponse(
|
||||
document, Filenames.toSimpleFileName(file.getOriginalFilename()));
|
||||
}
|
||||
|
||||
@@ -14,8 +14,6 @@ import java.util.zip.ZipOutputStream;
|
||||
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.rendering.PDFRenderer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
@@ -37,16 +35,17 @@ import io.github.pixee.security.Filenames;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.SPDF.model.api.misc.AutoSplitPdfRequest;
|
||||
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/misc")
|
||||
@Slf4j
|
||||
@Tag(name = "Misc", description = "Miscellaneous APIs")
|
||||
public class AutoSplitPdfController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(AutoSplitPdfController.class);
|
||||
private static final String QR_CONTENT = "https://github.com/Stirling-Tools/Stirling-PDF";
|
||||
private static final String QR_CONTENT_OLD = "https://github.com/Frooodle/Stirling-PDF";
|
||||
|
||||
@@ -134,7 +133,7 @@ public class AutoSplitPdfController {
|
||||
try {
|
||||
document.close();
|
||||
} catch (IOException e) {
|
||||
logger.error("Error closing main PDDocument", e);
|
||||
log.error("Error closing main PDDocument", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,7 +141,7 @@ public class AutoSplitPdfController {
|
||||
try {
|
||||
splitDoc.close();
|
||||
} catch (IOException e) {
|
||||
logger.error("Error closing split PDDocument", e);
|
||||
log.error("Error closing split PDDocument", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,7 +149,7 @@ public class AutoSplitPdfController {
|
||||
try {
|
||||
Files.deleteIfExists(zipFile);
|
||||
} catch (IOException e) {
|
||||
logger.error("Error deleting temporary zip file", e);
|
||||
log.error("Error deleting temporary zip file", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,8 +14,6 @@ import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.apache.pdfbox.pdmodel.PDPageTree;
|
||||
import org.apache.pdfbox.rendering.PDFRenderer;
|
||||
import org.apache.pdfbox.text.PDFTextStripper;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
@@ -30,6 +28,7 @@ import io.github.pixee.security.Filenames;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.SPDF.model.api.misc.RemoveBlankPagesRequest;
|
||||
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
||||
import stirling.software.SPDF.utils.PdfUtils;
|
||||
@@ -37,11 +36,10 @@ import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/misc")
|
||||
@Slf4j
|
||||
@Tag(name = "Misc", description = "Miscellaneous APIs")
|
||||
public class BlankPageController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(BlankPageController.class);
|
||||
|
||||
private final CustomPDDocumentFactory pdfDocumentFactory;
|
||||
|
||||
@Autowired
|
||||
@@ -71,7 +69,7 @@ public class BlankPageController {
|
||||
PDFRenderer pdfRenderer = new PDFRenderer(document);
|
||||
pdfRenderer.setSubsamplingAllowed(true);
|
||||
for (PDPage page : pages) {
|
||||
logger.info("checking page {}", pageIndex);
|
||||
log.info("checking page {}", pageIndex);
|
||||
textStripper.setStartPage(pageIndex + 1);
|
||||
textStripper.setEndPage(pageIndex + 1);
|
||||
String pageText = textStripper.getText(document);
|
||||
@@ -79,12 +77,12 @@ public class BlankPageController {
|
||||
|
||||
boolean blank = true;
|
||||
if (hasText) {
|
||||
logger.info("page {} has text, not blank", pageIndex);
|
||||
log.info("page {} has text, not blank", pageIndex);
|
||||
blank = false;
|
||||
} else {
|
||||
boolean hasImages = PdfUtils.hasImagesOnPage(page);
|
||||
if (hasImages) {
|
||||
logger.info("page {} has image, running blank detection", pageIndex);
|
||||
log.info("page {} has image, running blank detection", pageIndex);
|
||||
// Render image and save as temp file
|
||||
BufferedImage image = pdfRenderer.renderImageWithDPI(pageIndex, 30);
|
||||
blank = isBlankImage(image, threshold, whitePercent, threshold);
|
||||
@@ -92,10 +90,10 @@ public class BlankPageController {
|
||||
}
|
||||
|
||||
if (blank) {
|
||||
logger.info("Skipping, Image was blank for page #{}", pageIndex);
|
||||
log.info("Skipping, Image was blank for page #{}", pageIndex);
|
||||
blankPages.add(page);
|
||||
} else {
|
||||
logger.info("page {} has image which is not blank", pageIndex);
|
||||
log.info("page {} has image which is not blank", pageIndex);
|
||||
nonBlankPages.add(page);
|
||||
}
|
||||
|
||||
@@ -121,12 +119,12 @@ public class BlankPageController {
|
||||
|
||||
zos.close();
|
||||
|
||||
logger.info("Returning ZIP file: {}", filename + "_processed.zip");
|
||||
log.info("Returning ZIP file: {}", filename + "_processed.zip");
|
||||
return WebResponseUtils.boasToWebResponse(
|
||||
baos, filename + "_processed.zip", MediaType.APPLICATION_OCTET_STREAM);
|
||||
|
||||
} catch (IOException e) {
|
||||
logger.error("exception", e);
|
||||
log.error("exception", e);
|
||||
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
@@ -149,7 +147,7 @@ public class BlankPageController {
|
||||
public static boolean isBlankImage(
|
||||
BufferedImage image, int threshold, double whitePercent, int blurSize) {
|
||||
if (image == null) {
|
||||
logger.info("Error: Image is null");
|
||||
log.info("Error: Image is null");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -167,7 +165,7 @@ public class BlankPageController {
|
||||
}
|
||||
|
||||
double whitePixelPercentage = (whitePixels / (double) totalPixels) * 100;
|
||||
logger.info(String.format("Page has white pixel percent of %.2f%%", whitePixelPercentage));
|
||||
log.info(String.format("Page has white pixel percent of %.2f%%", whitePixelPercentage));
|
||||
|
||||
return whitePixelPercentage >= whitePercent;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import java.util.List;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.pdfbox.Loader;
|
||||
import org.apache.pdfbox.cos.COSName;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
@@ -18,8 +17,6 @@ import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.apache.pdfbox.pdmodel.PDResources;
|
||||
import org.apache.pdfbox.pdmodel.graphics.PDXObject;
|
||||
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
@@ -32,6 +29,7 @@ import io.github.pixee.security.Filenames;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.SPDF.model.api.misc.OptimizePdfRequest;
|
||||
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
||||
import stirling.software.SPDF.utils.GeneralUtils;
|
||||
@@ -41,11 +39,10 @@ import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/misc")
|
||||
@Slf4j
|
||||
@Tag(name = "Misc", description = "Miscellaneous APIs")
|
||||
public class CompressController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(CompressController.class);
|
||||
|
||||
private final CustomPDDocumentFactory pdfDocumentFactory;
|
||||
|
||||
@Autowired
|
||||
@@ -53,6 +50,54 @@ public class CompressController {
|
||||
this.pdfDocumentFactory = pdfDocumentFactory;
|
||||
}
|
||||
|
||||
private void compressImagesInPDF(Path pdfFile, double initialScaleFactor) throws Exception {
|
||||
byte[] fileBytes = Files.readAllBytes(pdfFile);
|
||||
try (PDDocument doc = Loader.loadPDF(fileBytes)) {
|
||||
double scaleFactor = initialScaleFactor;
|
||||
|
||||
for (PDPage page : doc.getPages()) {
|
||||
PDResources res = page.getResources();
|
||||
if (res != null && res.getXObjectNames() != null) {
|
||||
for (COSName name : res.getXObjectNames()) {
|
||||
PDXObject xobj = res.getXObject(name);
|
||||
if (xobj instanceof PDImageXObject) {
|
||||
PDImageXObject image = (PDImageXObject) xobj;
|
||||
BufferedImage bufferedImage = image.getImage();
|
||||
|
||||
int newWidth = (int) (bufferedImage.getWidth() * scaleFactor);
|
||||
int newHeight = (int) (bufferedImage.getHeight() * scaleFactor);
|
||||
|
||||
if (newWidth == 0 || newHeight == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Image scaledImage =
|
||||
bufferedImage.getScaledInstance(
|
||||
newWidth, newHeight, Image.SCALE_SMOOTH);
|
||||
|
||||
BufferedImage scaledBufferedImage =
|
||||
new BufferedImage(
|
||||
newWidth, newHeight, BufferedImage.TYPE_INT_RGB);
|
||||
scaledBufferedImage.getGraphics().drawImage(scaledImage, 0, 0, null);
|
||||
|
||||
ByteArrayOutputStream compressedImageStream =
|
||||
new ByteArrayOutputStream();
|
||||
ImageIO.write(scaledBufferedImage, "jpeg", compressedImageStream);
|
||||
byte[] imageBytes = compressedImageStream.toByteArray();
|
||||
compressedImageStream.close();
|
||||
|
||||
PDImageXObject compressedImage =
|
||||
PDImageXObject.createFromByteArray(
|
||||
doc, imageBytes, image.getCOSObject().toString());
|
||||
res.put(name, compressedImage);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
doc.save(pdfFile.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/compress-pdf")
|
||||
@Operation(
|
||||
summary = "Optimize PDF file",
|
||||
@@ -75,209 +120,92 @@ public class CompressController {
|
||||
autoMode = true;
|
||||
}
|
||||
|
||||
// Save the uploaded file to a temporary location
|
||||
Path tempInputFile = Files.createTempFile("input_", ".pdf");
|
||||
inputFile.transferTo(tempInputFile.toFile());
|
||||
|
||||
long inputFileSize = Files.size(tempInputFile);
|
||||
|
||||
// Prepare the output file path
|
||||
|
||||
Path tempOutputFile = null;
|
||||
byte[] pdfBytes;
|
||||
try {
|
||||
tempOutputFile = Files.createTempFile("output_", ".pdf");
|
||||
// Determine initial optimization level based on expected size reduction, only if in
|
||||
// autoMode
|
||||
|
||||
if (autoMode) {
|
||||
double sizeReductionRatio = expectedOutputSize / (double) inputFileSize;
|
||||
if (sizeReductionRatio > 0.7) {
|
||||
optimizeLevel = 1;
|
||||
} else if (sizeReductionRatio > 0.5) {
|
||||
optimizeLevel = 2;
|
||||
} else if (sizeReductionRatio > 0.35) {
|
||||
optimizeLevel = 3;
|
||||
} else {
|
||||
optimizeLevel = 3;
|
||||
}
|
||||
optimizeLevel = determineOptimizeLevel(sizeReductionRatio);
|
||||
}
|
||||
|
||||
boolean sizeMet = false;
|
||||
while (!sizeMet && optimizeLevel <= 4) {
|
||||
// Prepare the Ghostscript command
|
||||
List<String> command = new ArrayList<>();
|
||||
command.add("gs");
|
||||
command.add("-sDEVICE=pdfwrite");
|
||||
command.add("-dCompatibilityLevel=1.5");
|
||||
while (!sizeMet && optimizeLevel <= 9) {
|
||||
|
||||
switch (optimizeLevel) {
|
||||
case 1:
|
||||
command.add("-dPDFSETTINGS=/prepress");
|
||||
break;
|
||||
case 2:
|
||||
command.add("-dPDFSETTINGS=/printer");
|
||||
break;
|
||||
case 3:
|
||||
command.add("-dPDFSETTINGS=/ebook");
|
||||
break;
|
||||
case 4:
|
||||
command.add("-dPDFSETTINGS=/screen");
|
||||
break;
|
||||
default:
|
||||
command.add("-dPDFSETTINGS=/default");
|
||||
// Apply additional image compression for levels 6-9
|
||||
if (optimizeLevel >= 6) {
|
||||
// Calculate scale factor based on optimization level
|
||||
double scaleFactor =
|
||||
switch (optimizeLevel) {
|
||||
case 6 -> 0.9; // 90% of original size
|
||||
case 7 -> 0.8; // 80% of original size
|
||||
case 8 -> 0.65; // 70% of original size
|
||||
case 9 -> 0.5; // 60% of original size
|
||||
default -> 1.0;
|
||||
};
|
||||
compressImagesInPDF(tempInputFile, scaleFactor);
|
||||
}
|
||||
|
||||
command.add("-dNOPAUSE");
|
||||
command.add("-dQUIET");
|
||||
command.add("-dBATCH");
|
||||
command.add("-sOutputFile=" + tempOutputFile.toString());
|
||||
// Run QPDF optimization
|
||||
List<String> command = new ArrayList<>();
|
||||
command.add("qpdf");
|
||||
if (request.getNormalize()) {
|
||||
command.add("--normalize-content=y");
|
||||
}
|
||||
if (request.getLinearize()) {
|
||||
command.add("--linearize");
|
||||
}
|
||||
command.add("--optimize-images");
|
||||
command.add("--recompress-flate");
|
||||
command.add("--compression-level=" + optimizeLevel);
|
||||
command.add("--compress-streams=y");
|
||||
command.add("--object-streams=generate");
|
||||
command.add(tempInputFile.toString());
|
||||
command.add(tempOutputFile.toString());
|
||||
|
||||
ProcessExecutorResult returnCode =
|
||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT)
|
||||
.runCommandWithOutputHandling(command);
|
||||
ProcessExecutorResult returnCode = null;
|
||||
try {
|
||||
returnCode =
|
||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.QPDF)
|
||||
.runCommandWithOutputHandling(command);
|
||||
} catch (Exception e) {
|
||||
if (returnCode != null && returnCode.getRc() != 3) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if file size is within expected size or not auto mode so instantly finish
|
||||
// Check if file size is within expected size or not auto mode
|
||||
long outputFileSize = Files.size(tempOutputFile);
|
||||
if (outputFileSize <= expectedOutputSize || !autoMode) {
|
||||
sizeMet = true;
|
||||
} else {
|
||||
// Increase optimization level for next iteration
|
||||
optimizeLevel++;
|
||||
if (autoMode && optimizeLevel > 4) {
|
||||
logger.info("Skipping level 5 due to bad results in auto mode");
|
||||
optimizeLevel =
|
||||
incrementOptimizeLevel(
|
||||
optimizeLevel, outputFileSize, expectedOutputSize);
|
||||
if (autoMode && optimizeLevel > 9) {
|
||||
log.info("Maximum compression level reached in auto mode");
|
||||
sizeMet = true;
|
||||
} else {
|
||||
logger.info(
|
||||
"Increasing ghostscript optimisation level to " + optimizeLevel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (expectedOutputSize != null && autoMode) {
|
||||
long outputFileSize = Files.size(tempOutputFile);
|
||||
byte[] fileBytes = Files.readAllBytes(tempOutputFile);
|
||||
if (outputFileSize > expectedOutputSize) {
|
||||
try (PDDocument doc = Loader.loadPDF(fileBytes)) {
|
||||
long previousFileSize = 0;
|
||||
double scaleFactorConst = 0.9f;
|
||||
double scaleFactor = 0.9f;
|
||||
while (true) {
|
||||
for (PDPage page : doc.getPages()) {
|
||||
PDResources res = page.getResources();
|
||||
if (res != null && res.getXObjectNames() != null) {
|
||||
for (COSName name : res.getXObjectNames()) {
|
||||
PDXObject xobj = res.getXObject(name);
|
||||
if (xobj != null && xobj instanceof PDImageXObject) {
|
||||
PDImageXObject image = (PDImageXObject) xobj;
|
||||
|
||||
// Get the image in BufferedImage format
|
||||
BufferedImage bufferedImage = image.getImage();
|
||||
|
||||
// Calculate the new dimensions
|
||||
int newWidth =
|
||||
(int)
|
||||
(bufferedImage.getWidth()
|
||||
* scaleFactorConst);
|
||||
int newHeight =
|
||||
(int)
|
||||
(bufferedImage.getHeight()
|
||||
* scaleFactorConst);
|
||||
|
||||
// If the new dimensions are zero, skip this iteration
|
||||
if (newWidth == 0 || newHeight == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Otherwise, proceed with the scaling
|
||||
Image scaledImage =
|
||||
bufferedImage.getScaledInstance(
|
||||
newWidth,
|
||||
newHeight,
|
||||
Image.SCALE_SMOOTH);
|
||||
|
||||
// Convert the scaled image back to a BufferedImage
|
||||
BufferedImage scaledBufferedImage =
|
||||
new BufferedImage(
|
||||
newWidth,
|
||||
newHeight,
|
||||
BufferedImage.TYPE_INT_RGB);
|
||||
scaledBufferedImage
|
||||
.getGraphics()
|
||||
.drawImage(scaledImage, 0, 0, null);
|
||||
|
||||
// Compress the scaled image
|
||||
ByteArrayOutputStream compressedImageStream =
|
||||
new ByteArrayOutputStream();
|
||||
ImageIO.write(
|
||||
scaledBufferedImage,
|
||||
"jpeg",
|
||||
compressedImageStream);
|
||||
byte[] imageBytes = compressedImageStream.toByteArray();
|
||||
compressedImageStream.close();
|
||||
|
||||
PDImageXObject compressedImage =
|
||||
PDImageXObject.createFromByteArray(
|
||||
doc,
|
||||
imageBytes,
|
||||
image.getCOSObject().toString());
|
||||
|
||||
// Replace the image in the resources with the
|
||||
// compressed
|
||||
// version
|
||||
res.put(name, compressedImage);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// save the document to tempOutputFile again
|
||||
doc.save(tempOutputFile.toString());
|
||||
|
||||
long currentSize = Files.size(tempOutputFile);
|
||||
// Check if the overall PDF size is still larger than expectedOutputSize
|
||||
if (currentSize > expectedOutputSize) {
|
||||
// Log the current file size and scaleFactor
|
||||
|
||||
logger.info(
|
||||
"Current file size: "
|
||||
+ FileUtils.byteCountToDisplaySize(currentSize));
|
||||
logger.info("Current scale factor: " + scaleFactor);
|
||||
|
||||
// The file is still too large, reduce scaleFactor and try again
|
||||
scaleFactor *= 0.9f; // reduce scaleFactor by 10%
|
||||
// Avoid scaleFactor being too small, causing the image to shrink to
|
||||
// 0
|
||||
if (scaleFactor < 0.2f || previousFileSize == currentSize) {
|
||||
throw new RuntimeException(
|
||||
"Could not reach the desired size without excessively degrading image quality, lowest size recommended is "
|
||||
+ FileUtils.byteCountToDisplaySize(currentSize)
|
||||
+ ", "
|
||||
+ currentSize
|
||||
+ " bytes");
|
||||
}
|
||||
previousFileSize = currentSize;
|
||||
} else {
|
||||
// The file is small enough, break the loop
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Read the optimized PDF file
|
||||
pdfBytes = Files.readAllBytes(tempOutputFile);
|
||||
Path finalFile = tempOutputFile;
|
||||
|
||||
// Check if optimized file is larger than the original
|
||||
if (pdfBytes.length > inputFileSize) {
|
||||
// Log the occurrence
|
||||
logger.warn(
|
||||
log.warn(
|
||||
"Optimized file is larger than the original. Returning the original file instead.");
|
||||
|
||||
// Read the original file again
|
||||
finalFile = tempInputFile;
|
||||
}
|
||||
// Return the optimized PDF as a response
|
||||
|
||||
String outputFilename =
|
||||
Filenames.toSimpleFileName(inputFile.getOriginalFilename())
|
||||
.replaceFirst("[.][^.]+$", "")
|
||||
@@ -286,10 +214,31 @@ public class CompressController {
|
||||
pdfDocumentFactory.load(finalFile.toFile()), outputFilename);
|
||||
|
||||
} finally {
|
||||
// Clean up the temporary files
|
||||
// deleted by multipart file handler deu to transferTo?
|
||||
// Files.deleteIfExists(tempInputFile);
|
||||
Files.deleteIfExists(tempOutputFile);
|
||||
}
|
||||
}
|
||||
|
||||
private int determineOptimizeLevel(double sizeReductionRatio) {
|
||||
if (sizeReductionRatio > 0.9) return 1;
|
||||
if (sizeReductionRatio > 0.8) return 2;
|
||||
if (sizeReductionRatio > 0.7) return 3;
|
||||
if (sizeReductionRatio > 0.6) return 4;
|
||||
if (sizeReductionRatio > 0.5) return 5;
|
||||
if (sizeReductionRatio > 0.4) return 6;
|
||||
if (sizeReductionRatio > 0.3) return 7;
|
||||
if (sizeReductionRatio > 0.2) return 8;
|
||||
return 9;
|
||||
}
|
||||
|
||||
private int incrementOptimizeLevel(int currentLevel, long currentSize, long targetSize) {
|
||||
double currentRatio = currentSize / (double) targetSize;
|
||||
log.info("Current compression ratio: {}", String.format("%.2f", currentRatio));
|
||||
|
||||
if (currentRatio > 2.0) {
|
||||
return Math.min(9, currentLevel + 3);
|
||||
} else if (currentRatio > 1.5) {
|
||||
return Math.min(9, currentLevel + 2);
|
||||
}
|
||||
return Math.min(9, currentLevel + 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,8 +17,6 @@ import org.apache.commons.io.FileUtils;
|
||||
import org.apache.pdfbox.Loader;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.rendering.PDFRenderer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
@@ -31,6 +29,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.SPDF.model.api.misc.ExtractImageScansRequest;
|
||||
import stirling.software.SPDF.utils.CheckProgramInstall;
|
||||
import stirling.software.SPDF.utils.ProcessExecutor;
|
||||
@@ -39,11 +38,10 @@ import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/misc")
|
||||
@Slf4j
|
||||
@Tag(name = "Misc", description = "Miscellaneous APIs")
|
||||
public class ExtractImageScansController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ExtractImageScansController.class);
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/extract-image-scans")
|
||||
@Operation(
|
||||
summary = "Extract image scans from an input file",
|
||||
@@ -201,7 +199,7 @@ public class ExtractImageScansController {
|
||||
try {
|
||||
Files.deleteIfExists(path);
|
||||
} catch (IOException e) {
|
||||
logger.error("Failed to delete temporary image file: " + path, e);
|
||||
log.error("Failed to delete temporary image file: " + path, e);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -209,7 +207,7 @@ public class ExtractImageScansController {
|
||||
try {
|
||||
Files.deleteIfExists(tempZipFile);
|
||||
} catch (IOException e) {
|
||||
logger.error("Failed to delete temporary zip file: " + tempZipFile, e);
|
||||
log.error("Failed to delete temporary zip file: " + tempZipFile, e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,7 +216,7 @@ public class ExtractImageScansController {
|
||||
try {
|
||||
FileUtils.deleteDirectory(dir.toFile());
|
||||
} catch (IOException e) {
|
||||
logger.error("Failed to delete temporary directory: " + dir, e);
|
||||
log.error("Failed to delete temporary directory: " + dir, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -25,8 +25,6 @@ import org.apache.pdfbox.cos.COSName;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
@@ -39,17 +37,17 @@ import io.github.pixee.security.Filenames;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.SPDF.model.api.PDFExtractImagesRequest;
|
||||
import stirling.software.SPDF.utils.ImageProcessingUtils;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/misc")
|
||||
@Slf4j
|
||||
@Tag(name = "Misc", description = "Miscellaneous APIs")
|
||||
public class ExtractImagesController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ExtractImagesController.class);
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/extract-images")
|
||||
@Operation(
|
||||
summary = "Extract images from a PDF file",
|
||||
@@ -107,7 +105,7 @@ public class ExtractImagesController {
|
||||
allowDuplicates);
|
||||
} catch (IOException e) {
|
||||
// Log the error and continue processing other pages
|
||||
logger.error(
|
||||
log.error(
|
||||
"Error extracting images from page {}: {}",
|
||||
pageNum,
|
||||
e.getMessage());
|
||||
@@ -167,7 +165,7 @@ public class ExtractImagesController {
|
||||
try {
|
||||
md = MessageDigest.getInstance("MD5");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
logger.error("MD5 algorithm not available for extractImages hash.", e);
|
||||
log.error("MD5 algorithm not available for extractImages hash.", e);
|
||||
return;
|
||||
}
|
||||
if (page.getResources() == null || page.getResources().getXObjectNames() == null) {
|
||||
|
||||
@@ -27,8 +27,6 @@ import org.apache.pdfbox.pdmodel.graphics.image.JPEGFactory;
|
||||
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
|
||||
import org.apache.pdfbox.rendering.ImageType;
|
||||
import org.apache.pdfbox.rendering.PDFRenderer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
@@ -50,15 +48,13 @@ import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
@Tag(name = "Misc", description = "Miscellaneous APIs")
|
||||
public class FakeScanControllerWIP {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(FakeScanControllerWIP.class);
|
||||
|
||||
// TODO finish
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/fake-scan")
|
||||
@Hidden
|
||||
@Operation(
|
||||
summary = "Repair a PDF file",
|
||||
description =
|
||||
"This endpoint repairs a given PDF file by running Ghostscript command. The PDF is first saved to a temporary location, repaired, read back, and then returned as a response.")
|
||||
"This endpoint repairs a given PDF file by running qpdf command. The PDF is first saved to a temporary location, repaired, read back, and then returned as a response.")
|
||||
public ResponseEntity<byte[]> fakeScan(@ModelAttribute PDFFile request) throws IOException {
|
||||
MultipartFile inputFile = request.getFileInput();
|
||||
|
||||
|
||||
@@ -12,8 +12,6 @@ import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
|
||||
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
|
||||
import org.apache.pdfbox.rendering.ImageType;
|
||||
import org.apache.pdfbox.rendering.PDFRenderer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
@@ -26,17 +24,17 @@ import io.github.pixee.security.Filenames;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.SPDF.model.api.misc.FlattenRequest;
|
||||
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/misc")
|
||||
@Slf4j
|
||||
@Tag(name = "Misc", description = "Miscellaneous APIs")
|
||||
public class FlattenController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(FlattenController.class);
|
||||
|
||||
private final CustomPDDocumentFactory pdfDocumentFactory;
|
||||
|
||||
@Autowired
|
||||
@@ -84,7 +82,7 @@ public class FlattenController {
|
||||
contentStream.drawImage(pdImage, 0, 0, pageWidth, pageHeight);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.error("exception", e);
|
||||
log.error("exception", e);
|
||||
}
|
||||
}
|
||||
return WebResponseUtils.pdfDocToWebResponse(
|
||||
|
||||
@@ -11,9 +11,9 @@ import org.apache.pdfbox.Loader;
|
||||
import org.apache.pdfbox.cos.COSName;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDDocumentInformation;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.WebDataBinder;
|
||||
import org.springframework.web.bind.annotation.InitBinder;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
@@ -24,16 +24,17 @@ import io.github.pixee.security.Filenames;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.SPDF.model.api.misc.MetadataRequest;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
import stirling.software.SPDF.utils.propertyeditor.StringToMapPropertyEditor;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/misc")
|
||||
@Slf4j
|
||||
@Tag(name = "Misc", description = "Miscellaneous APIs")
|
||||
public class MetadataController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(MetadataController.class);
|
||||
|
||||
private String checkUndefined(String entry) {
|
||||
// Check if the string is "undefined"
|
||||
if ("undefined".equals(entry)) {
|
||||
@@ -44,6 +45,11 @@ public class MetadataController {
|
||||
return entry;
|
||||
}
|
||||
|
||||
@InitBinder
|
||||
public void initBinder(WebDataBinder binder) {
|
||||
binder.registerCustomEditor(Map.class, "allRequestParams", new StringToMapPropertyEditor());
|
||||
}
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/update-metadata")
|
||||
@Operation(
|
||||
summary = "Update metadata of a PDF file",
|
||||
@@ -140,7 +146,7 @@ public class MetadataController {
|
||||
creationDateCal.setTime(
|
||||
new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").parse(creationDate));
|
||||
} catch (ParseException e) {
|
||||
logger.error("exception", e);
|
||||
log.error("exception", e);
|
||||
}
|
||||
info.setCreationDate(creationDateCal);
|
||||
} else {
|
||||
@@ -152,7 +158,7 @@ public class MetadataController {
|
||||
modificationDateCal.setTime(
|
||||
new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").parse(modificationDate));
|
||||
} catch (ParseException e) {
|
||||
logger.error("exception", e);
|
||||
log.error("exception", e);
|
||||
}
|
||||
info.setModificationDate(modificationDateCal);
|
||||
} else {
|
||||
|
||||
@@ -1,19 +1,29 @@
|
||||
package stirling.software.SPDF.controller.api.misc;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
|
||||
import org.apache.pdfbox.multipdf.PDFMergerUtility;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.apache.pdfbox.rendering.PDFRenderer;
|
||||
import org.apache.pdfbox.text.PDFTextStripper;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
@@ -23,24 +33,31 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.github.pixee.security.BoundedLineReader;
|
||||
import io.github.pixee.security.Filenames;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.SPDF.model.ApplicationProperties;
|
||||
import stirling.software.SPDF.model.api.misc.ProcessPdfWithOcrRequest;
|
||||
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
||||
import stirling.software.SPDF.utils.ProcessExecutor;
|
||||
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/misc")
|
||||
@Tag(name = "Misc", description = "Miscellaneous APIs")
|
||||
@Slf4j
|
||||
public class OCRController {
|
||||
|
||||
@Autowired ApplicationProperties applicationProperties;
|
||||
@Autowired private ApplicationProperties applicationProperties;
|
||||
|
||||
private final CustomPDDocumentFactory pdfDocumentFactory;
|
||||
|
||||
@Autowired
|
||||
public OCRController(CustomPDDocumentFactory pdfDocumentFactory) {
|
||||
this.pdfDocumentFactory = pdfDocumentFactory;
|
||||
}
|
||||
|
||||
/** Gets the list of available Tesseract languages from the tessdata directory */
|
||||
public List<String> getAvailableTesseractLanguages() {
|
||||
String tessdataDir = applicationProperties.getSystem().getTessdataDir();
|
||||
File[] files = new File(tessdataDir).listFiles();
|
||||
@@ -54,196 +71,167 @@ public class OCRController {
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private final CustomPDDocumentFactory pdfDocumentFactory;
|
||||
|
||||
@Autowired
|
||||
public OCRController(CustomPDDocumentFactory pdfDocumentFactory) {
|
||||
this.pdfDocumentFactory = pdfDocumentFactory;
|
||||
}
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/ocr-pdf")
|
||||
@Operation(
|
||||
summary = "Process a PDF file with OCR",
|
||||
description =
|
||||
"This endpoint processes a PDF file using OCR (Optical Character Recognition). Users can specify languages, sidecar, deskew, clean, cleanFinal, ocrType, ocrRenderType, and removeImagesAfter options. Input:PDF Output:PDF Type:SI-Conditional")
|
||||
public ResponseEntity<byte[]> processPdfWithOCR(
|
||||
@ModelAttribute ProcessPdfWithOcrRequest request)
|
||||
throws IOException, InterruptedException {
|
||||
MultipartFile inputFile = request.getFileInput();
|
||||
List<String> selectedLanguages = request.getLanguages();
|
||||
Boolean sidecar = request.isSidecar();
|
||||
Boolean deskew = request.isDeskew();
|
||||
Boolean clean = request.isClean();
|
||||
Boolean cleanFinal = request.isCleanFinal();
|
||||
List<String> languages = request.getLanguages();
|
||||
String ocrType = request.getOcrType();
|
||||
String ocrRenderType = request.getOcrRenderType();
|
||||
Boolean removeImagesAfter = request.isRemoveImagesAfter();
|
||||
// --output-type pdfa
|
||||
if (selectedLanguages == null || selectedLanguages.isEmpty()) {
|
||||
throw new IOException("Please select at least one language.");
|
||||
}
|
||||
|
||||
if (!"hocr".equals(ocrRenderType) && !"sandwich".equals(ocrRenderType)) {
|
||||
throw new IOException("ocrRenderType wrong");
|
||||
}
|
||||
|
||||
// Get available Tesseract languages
|
||||
List<String> availableLanguages = getAvailableTesseractLanguages();
|
||||
|
||||
// Validate selected languages
|
||||
selectedLanguages =
|
||||
selectedLanguages.stream().filter(availableLanguages::contains).toList();
|
||||
|
||||
if (selectedLanguages.isEmpty()) {
|
||||
throw new IOException("None of the selected languages are valid.");
|
||||
}
|
||||
// Save the uploaded file to a temporary location
|
||||
Path tempInputFile = Files.createTempFile("input_", ".pdf");
|
||||
Path tempOutputFile = Files.createTempFile("output_", ".pdf");
|
||||
Path sidecarTextPath = null;
|
||||
Path tempDir = Files.createTempDirectory("ocr_process");
|
||||
Path tempInputFile = tempDir.resolve("input.pdf");
|
||||
Path tempOutputDir = tempDir.resolve("output");
|
||||
Path tempImagesDir = tempDir.resolve("images");
|
||||
Path finalOutputFile = tempDir.resolve("final_output.pdf");
|
||||
|
||||
Files.createDirectories(tempOutputDir);
|
||||
Files.createDirectories(tempImagesDir);
|
||||
Process process = null;
|
||||
try {
|
||||
// Save input file
|
||||
inputFile.transferTo(tempInputFile.toFile());
|
||||
PDFMergerUtility merger = new PDFMergerUtility();
|
||||
merger.setDestinationFileName(finalOutputFile.toString());
|
||||
|
||||
// Run OCR Command
|
||||
String languageOption = String.join("+", selectedLanguages);
|
||||
try (PDDocument document = pdfDocumentFactory.load(tempInputFile.toFile())) {
|
||||
PDFRenderer pdfRenderer = new PDFRenderer(document);
|
||||
int pageCount = document.getNumberOfPages();
|
||||
|
||||
List<String> command =
|
||||
new ArrayList<>(
|
||||
Arrays.asList(
|
||||
"ocrmypdf",
|
||||
"--verbose",
|
||||
"2",
|
||||
"--output-type",
|
||||
"pdf",
|
||||
"--pdf-renderer",
|
||||
ocrRenderType));
|
||||
for (int pageNum = 0; pageNum < pageCount; pageNum++) {
|
||||
PDPage page = document.getPage(pageNum);
|
||||
boolean hasText = false;
|
||||
|
||||
if (sidecar != null && sidecar) {
|
||||
sidecarTextPath = Files.createTempFile("sidecar", ".txt");
|
||||
command.add("--sidecar");
|
||||
command.add(sidecarTextPath.toString());
|
||||
}
|
||||
// Check for existing text
|
||||
try (PDDocument tempDoc = new PDDocument()) {
|
||||
tempDoc.addPage(page);
|
||||
PDFTextStripper stripper = new PDFTextStripper();
|
||||
hasText = !stripper.getText(tempDoc).trim().isEmpty();
|
||||
}
|
||||
|
||||
if (deskew != null && deskew) {
|
||||
command.add("--deskew");
|
||||
}
|
||||
if (clean != null && clean) {
|
||||
command.add("--clean");
|
||||
}
|
||||
if (cleanFinal != null && cleanFinal) {
|
||||
command.add("--clean-final");
|
||||
}
|
||||
if (ocrType != null && !"".equals(ocrType)) {
|
||||
if ("skip-text".equals(ocrType)) {
|
||||
command.add("--skip-text");
|
||||
} else if ("force-ocr".equals(ocrType)) {
|
||||
command.add("--force-ocr");
|
||||
} else if ("Normal".equals(ocrType)) {
|
||||
boolean shouldOcr =
|
||||
switch (ocrType) {
|
||||
case "skip-text" -> !hasText;
|
||||
case "force-ocr" -> true;
|
||||
default -> true;
|
||||
};
|
||||
|
||||
Path pageOutputPath =
|
||||
tempOutputDir.resolve(String.format("page_%d.pdf", pageNum));
|
||||
|
||||
if (shouldOcr) {
|
||||
// Convert page to image
|
||||
BufferedImage image = pdfRenderer.renderImageWithDPI(pageNum, 300);
|
||||
Path imagePath =
|
||||
tempImagesDir.resolve(String.format("page_%d.png", pageNum));
|
||||
ImageIO.write(image, "png", imagePath.toFile());
|
||||
|
||||
// Build OCR command
|
||||
List<String> command = new ArrayList<>();
|
||||
command.add("tesseract");
|
||||
command.add(imagePath.toString());
|
||||
command.add(
|
||||
tempOutputDir
|
||||
.resolve(String.format("page_%d", pageNum))
|
||||
.toString());
|
||||
command.add("-l");
|
||||
command.add(String.join("+", languages));
|
||||
command.add("pdf"); // Always output PDF
|
||||
|
||||
ProcessBuilder pb = new ProcessBuilder(command);
|
||||
process = pb.start();
|
||||
|
||||
// Capture any error output
|
||||
try (BufferedReader reader =
|
||||
new BufferedReader(
|
||||
new InputStreamReader(process.getErrorStream()))) {
|
||||
String line;
|
||||
while ((line = BoundedLineReader.readLine(reader, 5_000_000)) != null) {
|
||||
log.debug("Tesseract: {}", line);
|
||||
}
|
||||
}
|
||||
|
||||
int exitCode = process.waitFor();
|
||||
if (exitCode != 0) {
|
||||
throw new RuntimeException(
|
||||
"Tesseract failed with exit code: " + exitCode);
|
||||
}
|
||||
|
||||
// Add OCR'd PDF to merger
|
||||
merger.addSource(pageOutputPath.toFile());
|
||||
} else {
|
||||
// Save original page without OCR
|
||||
try (PDDocument pageDoc = new PDDocument()) {
|
||||
pageDoc.addPage(page);
|
||||
pageDoc.save(pageOutputPath.toFile());
|
||||
merger.addSource(pageOutputPath.toFile());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
command.addAll(
|
||||
Arrays.asList(
|
||||
"--language",
|
||||
languageOption,
|
||||
tempInputFile.toString(),
|
||||
tempOutputFile.toString()));
|
||||
// Merge all pages into final PDF
|
||||
merger.mergeDocuments(null);
|
||||
|
||||
// Run CLI command
|
||||
ProcessExecutorResult result =
|
||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.OCR_MY_PDF)
|
||||
.runCommandWithOutputHandling(command);
|
||||
if (result.getRc() != 0
|
||||
&& result.getMessages().contains("multiprocessing/synchronize.py")
|
||||
&& result.getMessages()
|
||||
.contains("OSError: [Errno 38] Function not implemented")) {
|
||||
command.add("--jobs");
|
||||
command.add("1");
|
||||
result =
|
||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.OCR_MY_PDF)
|
||||
.runCommandWithOutputHandling(command);
|
||||
}
|
||||
|
||||
// Remove images from the OCR processed PDF if the flag is set to true
|
||||
if (removeImagesAfter != null && removeImagesAfter) {
|
||||
Path tempPdfWithoutImages = Files.createTempFile("output_", "_no_images.pdf");
|
||||
|
||||
List<String> gsCommand =
|
||||
Arrays.asList(
|
||||
"gs",
|
||||
"-sDEVICE=pdfwrite",
|
||||
"-dFILTERIMAGE",
|
||||
"-o",
|
||||
tempPdfWithoutImages.toString(),
|
||||
tempOutputFile.toString());
|
||||
|
||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT)
|
||||
.runCommandWithOutputHandling(gsCommand);
|
||||
tempOutputFile = tempPdfWithoutImages;
|
||||
}
|
||||
// Read the OCR processed PDF file
|
||||
byte[] pdfBytes = pdfDocumentFactory.loadToBytes(tempOutputFile.toFile());
|
||||
|
||||
// Return the OCR processed PDF as a response
|
||||
// Read the final PDF file
|
||||
byte[] pdfContent = Files.readAllBytes(finalOutputFile);
|
||||
String outputFilename =
|
||||
Filenames.toSimpleFileName(inputFile.getOriginalFilename())
|
||||
.replaceFirst("[.][^.]+$", "")
|
||||
+ "_OCR.pdf";
|
||||
|
||||
if (sidecar != null && sidecar) {
|
||||
// Create a zip file containing both the PDF and the text file
|
||||
String outputZipFilename =
|
||||
Filenames.toSimpleFileName(inputFile.getOriginalFilename())
|
||||
.replaceFirst("[.][^.]+$", "")
|
||||
+ "_OCR.zip";
|
||||
Path tempZipFile = Files.createTempFile("output_", ".zip");
|
||||
return ResponseEntity.ok()
|
||||
.header(
|
||||
"Content-Disposition",
|
||||
"attachment; filename=\"" + outputFilename + "\"")
|
||||
.contentType(MediaType.APPLICATION_PDF)
|
||||
.body(pdfContent);
|
||||
|
||||
try (ZipOutputStream zipOut =
|
||||
new ZipOutputStream(new FileOutputStream(tempZipFile.toFile()))) {
|
||||
// Add PDF file to the zip
|
||||
ZipEntry pdfEntry = new ZipEntry(outputFilename);
|
||||
zipOut.putNextEntry(pdfEntry);
|
||||
try (ByteArrayInputStream pdfInputStream = new ByteArrayInputStream(pdfBytes)) {
|
||||
byte[] buffer = new byte[1024];
|
||||
int length;
|
||||
while ((length = pdfInputStream.read(buffer)) != -1) {
|
||||
zipOut.write(buffer, 0, length);
|
||||
}
|
||||
}
|
||||
zipOut.closeEntry();
|
||||
|
||||
// Add text file to the zip
|
||||
ZipEntry txtEntry = new ZipEntry(outputFilename.replace(".pdf", ".txt"));
|
||||
zipOut.putNextEntry(txtEntry);
|
||||
Files.copy(sidecarTextPath, zipOut);
|
||||
zipOut.closeEntry();
|
||||
}
|
||||
|
||||
byte[] zipBytes = Files.readAllBytes(tempZipFile);
|
||||
|
||||
// Clean up the temporary zip file
|
||||
Files.deleteIfExists(tempZipFile);
|
||||
Files.deleteIfExists(tempOutputFile);
|
||||
Files.deleteIfExists(sidecarTextPath);
|
||||
|
||||
// Return the zip file containing both the PDF and the text file
|
||||
return WebResponseUtils.bytesToWebResponse(
|
||||
zipBytes, outputZipFilename, MediaType.APPLICATION_OCTET_STREAM);
|
||||
} else {
|
||||
// Return the OCR processed PDF as a response
|
||||
Files.deleteIfExists(tempOutputFile);
|
||||
return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename);
|
||||
}
|
||||
} finally {
|
||||
// Clean up the temporary files
|
||||
Files.deleteIfExists(tempOutputFile);
|
||||
// Comment out as transferTo makes multipart handle cleanup
|
||||
// Files.deleteIfExists(tempInputFile);
|
||||
if (sidecarTextPath != null) {
|
||||
Files.deleteIfExists(sidecarTextPath);
|
||||
if (process != null) {
|
||||
process.destroy();
|
||||
}
|
||||
|
||||
// Clean up temporary files
|
||||
deleteDirectory(tempDir);
|
||||
}
|
||||
}
|
||||
|
||||
private void addFileToZip(File file, String filename, ZipOutputStream zipOut)
|
||||
throws IOException {
|
||||
if (!file.exists()) {
|
||||
log.warn("File {} does not exist, skipping", file);
|
||||
return;
|
||||
}
|
||||
|
||||
try (FileInputStream fis = new FileInputStream(file)) {
|
||||
ZipEntry zipEntry = new ZipEntry(filename);
|
||||
zipOut.putNextEntry(zipEntry);
|
||||
|
||||
byte[] buffer = new byte[1024];
|
||||
int length;
|
||||
while ((length = fis.read(buffer)) >= 0) {
|
||||
zipOut.write(buffer, 0, length);
|
||||
}
|
||||
|
||||
zipOut.closeEntry();
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteDirectory(Path directory) {
|
||||
try {
|
||||
Files.walk(directory)
|
||||
.sorted(Comparator.reverseOrder())
|
||||
.forEach(
|
||||
path -> {
|
||||
try {
|
||||
Files.delete(path);
|
||||
} catch (IOException e) {
|
||||
log.error("Error deleting {}: {}", path, e.getMessage());
|
||||
}
|
||||
});
|
||||
} catch (IOException e) {
|
||||
log.error("Error walking directory {}: {}", directory, e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@ package stirling.software.SPDF.controller.api.misc;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
@@ -17,6 +15,7 @@ import io.github.pixee.security.Filenames;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.SPDF.model.api.misc.OverlayImageRequest;
|
||||
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
||||
import stirling.software.SPDF.utils.PdfUtils;
|
||||
@@ -24,11 +23,10 @@ import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/misc")
|
||||
@Slf4j
|
||||
@Tag(name = "Misc", description = "Miscellaneous APIs")
|
||||
public class OverlayImageController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(OverlayImageController.class);
|
||||
|
||||
private final CustomPDDocumentFactory pdfDocumentFactory;
|
||||
|
||||
@Autowired
|
||||
@@ -60,7 +58,7 @@ public class OverlayImageController {
|
||||
.replaceFirst("[.][^.]+$", "")
|
||||
+ "_overlayed.pdf");
|
||||
} catch (IOException e) {
|
||||
logger.error("Failed to add image to PDF", e);
|
||||
log.error("Failed to add image to PDF", e);
|
||||
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,6 @@ import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||
import org.apache.pdfbox.pdmodel.font.PDType1Font;
|
||||
import org.apache.pdfbox.pdmodel.font.Standard14Fonts;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
@@ -35,8 +33,6 @@ import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
@Tag(name = "Misc", description = "Miscellaneous APIs")
|
||||
public class PageNumbersController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(PageNumbersController.class);
|
||||
|
||||
private final CustomPDDocumentFactory pdfDocumentFactory;
|
||||
|
||||
@Autowired
|
||||
|
||||
@@ -6,8 +6,6 @@ import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
@@ -31,8 +29,6 @@ import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
@Tag(name = "Misc", description = "Miscellaneous APIs")
|
||||
public class RepairController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(RepairController.class);
|
||||
|
||||
private final CustomPDDocumentFactory pdfDocumentFactory;
|
||||
|
||||
@Autowired
|
||||
@@ -44,30 +40,29 @@ public class RepairController {
|
||||
@Operation(
|
||||
summary = "Repair a PDF file",
|
||||
description =
|
||||
"This endpoint repairs a given PDF file by running Ghostscript command. The PDF is first saved to a temporary location, repaired, read back, and then returned as a response. Input:PDF Output:PDF Type:SISO")
|
||||
"This endpoint repairs a given PDF file by running qpdf command. The PDF is first saved to a temporary location, repaired, read back, and then returned as a response. Input:PDF Output:PDF Type:SISO")
|
||||
public ResponseEntity<byte[]> repairPdf(@ModelAttribute PDFFile request)
|
||||
throws IOException, InterruptedException {
|
||||
MultipartFile inputFile = request.getFileInput();
|
||||
// Save the uploaded file to a temporary location
|
||||
Path tempInputFile = Files.createTempFile("input_", ".pdf");
|
||||
Path tempOutputFile = Files.createTempFile("output_", ".pdf");
|
||||
byte[] pdfBytes = null;
|
||||
inputFile.transferTo(tempInputFile.toFile());
|
||||
try {
|
||||
|
||||
List<String> command = new ArrayList<>();
|
||||
command.add("gs");
|
||||
command.add("-o");
|
||||
command.add(tempOutputFile.toString());
|
||||
command.add("-sDEVICE=pdfwrite");
|
||||
command.add("qpdf");
|
||||
command.add("--replace-input"); // Automatically fixes problems it can
|
||||
command.add("--qdf"); // Linearizes and normalizes PDF structure
|
||||
command.add("--object-streams=disable"); // Can help with some corruptions
|
||||
command.add(tempInputFile.toString());
|
||||
|
||||
ProcessExecutorResult returnCode =
|
||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT)
|
||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.QPDF)
|
||||
.runCommandWithOutputHandling(command);
|
||||
|
||||
// Read the optimized PDF file
|
||||
pdfBytes = pdfDocumentFactory.loadToBytes(tempOutputFile.toFile());
|
||||
pdfBytes = pdfDocumentFactory.loadToBytes(tempInputFile.toFile());
|
||||
|
||||
// Return the optimized PDF as a response
|
||||
String outputFilename =
|
||||
@@ -78,7 +73,6 @@ public class RepairController {
|
||||
} finally {
|
||||
// Clean up the temporary files
|
||||
Files.deleteIfExists(tempInputFile);
|
||||
Files.deleteIfExists(tempOutputFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,6 @@ import org.apache.pdfbox.Loader;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.common.PDNameTreeNode;
|
||||
import org.apache.pdfbox.pdmodel.interactive.action.PDActionJavaScript;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
@@ -29,8 +27,6 @@ import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
@Tag(name = "Misc", description = "Miscellaneous APIs")
|
||||
public class ShowJavascript {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ShowJavascript.class);
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/show-javascript")
|
||||
@Operation(
|
||||
summary = "Grabs all JS from a PDF and returns a single JS file with all code",
|
||||
|
||||
@@ -229,10 +229,22 @@ public class StampController {
|
||||
calculatePositionY(
|
||||
pageSize, position, calculateTextCapHeight(font, fontSize), margin);
|
||||
}
|
||||
// Split the stampText into multiple lines
|
||||
String[] lines = stampText.split("\\\\n");
|
||||
|
||||
// Calculate dynamic line height based on font ascent and descent
|
||||
float ascent = font.getFontDescriptor().getAscent();
|
||||
float descent = font.getFontDescriptor().getDescent();
|
||||
float lineHeight = ((ascent - descent) / 1000) * fontSize;
|
||||
|
||||
contentStream.beginText();
|
||||
contentStream.setTextMatrix(Matrix.getRotateInstance(Math.toRadians(rotation), x, y));
|
||||
contentStream.showText(stampText);
|
||||
for (int i = 0; i < lines.length; i++) {
|
||||
String line = lines[i];
|
||||
// Set the text matrix for each line with rotation
|
||||
contentStream.setTextMatrix(
|
||||
Matrix.getRotateInstance(Math.toRadians(rotation), x, y - (i * lineHeight)));
|
||||
contentStream.showText(line);
|
||||
}
|
||||
contentStream.endText();
|
||||
}
|
||||
|
||||
|
||||
@@ -7,8 +7,6 @@ import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
@@ -21,17 +19,17 @@ import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import jakarta.servlet.ServletContext;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.SPDF.SPdfApplication;
|
||||
import stirling.software.SPDF.model.ApiEndpoint;
|
||||
import stirling.software.SPDF.model.Role;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
public class ApiDocService {
|
||||
|
||||
private final Map<String, ApiEndpoint> apiDocumentation = new HashMap<>();
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ApiDocService.class);
|
||||
|
||||
@Autowired private ServletContext servletContext;
|
||||
|
||||
private String getApiDocsUrl() {
|
||||
@@ -135,7 +133,7 @@ public class ApiDocService {
|
||||
});
|
||||
} catch (Exception e) {
|
||||
// Handle exceptions
|
||||
logger.error("Error grabbing swagger doc, body result {}", apiDocsJson);
|
||||
log.error("Error grabbing swagger doc, body result {}", apiDocsJson);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,8 +8,6 @@ import java.util.Map;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.MediaType;
|
||||
@@ -26,6 +24,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.SPDF.model.ApplicationProperties;
|
||||
import stirling.software.SPDF.model.PipelineConfig;
|
||||
import stirling.software.SPDF.model.api.HandleDataRequest;
|
||||
@@ -33,11 +32,10 @@ import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/pipeline")
|
||||
@Slf4j
|
||||
@Tag(name = "Pipeline", description = "Pipeline APIs")
|
||||
public class PipelineController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(PipelineController.class);
|
||||
|
||||
final String watchedFoldersDir = "./pipeline/watchedFolders/";
|
||||
final String finishedFoldersDir = "./pipeline/finishedFolders/";
|
||||
@Autowired PipelineProcessor processor;
|
||||
@@ -56,7 +54,7 @@ public class PipelineController {
|
||||
return null;
|
||||
}
|
||||
PipelineConfig config = objectMapper.readValue(jsonString, PipelineConfig.class);
|
||||
logger.info("Received POST request to /handleData with {} files", files.length);
|
||||
log.info("Received POST request to /handleData with {} files", files.length);
|
||||
try {
|
||||
List<Resource> inputFiles = processor.generateInputFiles(files);
|
||||
if (inputFiles == null || inputFiles.size() == 0) {
|
||||
@@ -71,7 +69,7 @@ public class PipelineController {
|
||||
is.read(bytes);
|
||||
is.close();
|
||||
|
||||
logger.info("Returning single file response...");
|
||||
log.info("Returning single file response...");
|
||||
return WebResponseUtils.bytesToWebResponse(
|
||||
bytes, singleFile.getFilename(), MediaType.APPLICATION_OCTET_STREAM);
|
||||
} else if (outputFiles == null) {
|
||||
@@ -118,11 +116,11 @@ public class PipelineController {
|
||||
|
||||
zipOut.close();
|
||||
|
||||
logger.info("Returning zipped file response...");
|
||||
log.info("Returning zipped file response...");
|
||||
return WebResponseUtils.boasToWebResponse(
|
||||
baos, "output.zip", MediaType.APPLICATION_OCTET_STREAM);
|
||||
} catch (Exception e) {
|
||||
logger.error("Error handling data: ", e);
|
||||
log.error("Error handling data: ", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,8 +16,6 @@ import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.core.io.ByteArrayResource;
|
||||
@@ -27,14 +25,15 @@ import org.springframework.stereotype.Service;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.SPDF.model.PipelineConfig;
|
||||
import stirling.software.SPDF.model.PipelineOperation;
|
||||
import stirling.software.SPDF.utils.FileMonitor;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
public class PipelineDirectoryProcessor {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(PipelineDirectoryProcessor.class);
|
||||
@Autowired private ObjectMapper objectMapper;
|
||||
@Autowired private ApiDocService apiDocService;
|
||||
@Autowired PipelineProcessor processor;
|
||||
@@ -56,9 +55,9 @@ public class PipelineDirectoryProcessor {
|
||||
if (!Files.exists(watchedFolderPath)) {
|
||||
try {
|
||||
Files.createDirectories(watchedFolderPath);
|
||||
logger.info("Created directory: {}", watchedFolderPath);
|
||||
log.info("Created directory: {}", watchedFolderPath);
|
||||
} catch (IOException e) {
|
||||
logger.error("Error creating directory: {}", watchedFolderPath, e);
|
||||
log.error("Error creating directory: {}", watchedFolderPath, e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -71,21 +70,21 @@ public class PipelineDirectoryProcessor {
|
||||
handleDirectory(t);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Error handling directory: {}", t, e);
|
||||
log.error("Error handling directory: {}", t, e);
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
logger.error("Error walking through directory: {}", watchedFolderPath, e);
|
||||
log.error("Error walking through directory: {}", watchedFolderPath, e);
|
||||
}
|
||||
}
|
||||
|
||||
public void handleDirectory(Path dir) throws IOException {
|
||||
logger.info("Handling directory: {}", dir);
|
||||
log.info("Handling directory: {}", dir);
|
||||
Path processingDir = createProcessingDirectory(dir);
|
||||
|
||||
Optional<Path> jsonFileOptional = findJsonFile(dir);
|
||||
if (!jsonFileOptional.isPresent()) {
|
||||
logger.warn("No .JSON settings file found. No processing will happen for dir {}.", dir);
|
||||
log.warn("No .JSON settings file found. No processing will happen for dir {}.", dir);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -98,7 +97,7 @@ public class PipelineDirectoryProcessor {
|
||||
Path processingDir = dir.resolve("processing");
|
||||
if (!Files.exists(processingDir)) {
|
||||
Files.createDirectory(processingDir);
|
||||
logger.info("Created processing directory: {}", processingDir);
|
||||
log.info("Created processing directory: {}", processingDir);
|
||||
}
|
||||
return processingDir;
|
||||
}
|
||||
@@ -111,7 +110,7 @@ public class PipelineDirectoryProcessor {
|
||||
|
||||
private PipelineConfig readAndParseJson(Path jsonFile) throws IOException {
|
||||
String jsonString = new String(Files.readAllBytes(jsonFile), StandardCharsets.UTF_8);
|
||||
logger.debug("Reading JSON file: {}", jsonFile);
|
||||
log.debug("Reading JSON file: {}", jsonFile);
|
||||
return objectMapper.readValue(jsonString, PipelineConfig.class);
|
||||
}
|
||||
|
||||
@@ -121,7 +120,7 @@ public class PipelineDirectoryProcessor {
|
||||
validateOperation(operation);
|
||||
File[] files = collectFilesForProcessing(dir, jsonFile, operation);
|
||||
if (files == null || files.length == 0) {
|
||||
logger.debug("No files detected for {} ", dir);
|
||||
log.debug("No files detected for {} ", dir);
|
||||
return;
|
||||
}
|
||||
List<File> filesToProcess = prepareFilesForProcessing(files, processingDir);
|
||||
@@ -202,7 +201,7 @@ public class PipelineDirectoryProcessor {
|
||||
moveAndRenameFiles(outputFiles, config, dir);
|
||||
deleteOriginalFiles(filesToProcess, processingDir);
|
||||
} catch (Exception e) {
|
||||
logger.error("error during processing", e);
|
||||
log.error("error during processing", e);
|
||||
moveFilesBack(filesToProcess, processingDir);
|
||||
}
|
||||
}
|
||||
@@ -215,7 +214,7 @@ public class PipelineDirectoryProcessor {
|
||||
|
||||
if (!Files.exists(outputPath)) {
|
||||
Files.createDirectories(outputPath);
|
||||
logger.info("Created directory: {}", outputPath);
|
||||
log.info("Created directory: {}", outputPath);
|
||||
}
|
||||
|
||||
Path outputFile = outputPath.resolve(outputFileName);
|
||||
@@ -223,7 +222,7 @@ public class PipelineDirectoryProcessor {
|
||||
os.write(((ByteArrayResource) resource).getByteArray());
|
||||
}
|
||||
|
||||
logger.info("File moved and renamed to {}", outputFile);
|
||||
log.info("File moved and renamed to {}", outputFile);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -264,7 +263,7 @@ public class PipelineDirectoryProcessor {
|
||||
throws IOException {
|
||||
for (File file : filesToProcess) {
|
||||
Files.deleteIfExists(processingDir.resolve(file.getName()));
|
||||
logger.info("Deleted original file: {}", file.getName());
|
||||
log.info("Deleted original file: {}", file.getName());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -272,12 +271,12 @@ public class PipelineDirectoryProcessor {
|
||||
for (File file : filesToProcess) {
|
||||
try {
|
||||
Files.move(processingDir.resolve(file.getName()), file.toPath());
|
||||
logger.info(
|
||||
log.info(
|
||||
"Moved file back to original location: {} , {}",
|
||||
file.toPath(),
|
||||
file.getName());
|
||||
} catch (IOException e) {
|
||||
logger.error("Error moving file back to original location: {}", file.getName(), e);
|
||||
log.error("Error moving file back to original location: {}", file.getName(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,8 +19,6 @@ import java.util.stream.Collectors;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.io.ByteArrayResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
@@ -40,16 +38,16 @@ import io.github.pixee.security.Filenames;
|
||||
import io.github.pixee.security.ZipSecurity;
|
||||
|
||||
import jakarta.servlet.ServletContext;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.SPDF.SPdfApplication;
|
||||
import stirling.software.SPDF.model.PipelineConfig;
|
||||
import stirling.software.SPDF.model.PipelineOperation;
|
||||
import stirling.software.SPDF.model.Role;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
public class PipelineProcessor {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(PipelineProcessor.class);
|
||||
|
||||
@Autowired private ApiDocService apiDocService;
|
||||
|
||||
@Autowired(required = false)
|
||||
@@ -81,7 +79,7 @@ public class PipelineProcessor {
|
||||
String operation = pipelineOperation.getOperation();
|
||||
boolean isMultiInputOperation = apiDocService.isMultiInput(operation);
|
||||
|
||||
logger.info(
|
||||
log.info(
|
||||
"Running operation: {} isMultiInputOperation {}",
|
||||
operation,
|
||||
isMultiInputOperation);
|
||||
@@ -124,7 +122,7 @@ public class PipelineProcessor {
|
||||
if (operation.startsWith("filter-")
|
||||
&& (response.getBody() == null
|
||||
|| response.getBody().length == 0)) {
|
||||
logger.info("Skipping file due to failing {}", operation);
|
||||
log.info("Skipping file due to failing {}", operation);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -208,7 +206,7 @@ public class PipelineProcessor {
|
||||
outputFiles = newOutputFiles;
|
||||
}
|
||||
if (hasErrors) {
|
||||
logger.error("Errors occurred during processing. Log: {}", logStream.toString());
|
||||
log.error("Errors occurred during processing. Log: {}", logStream.toString());
|
||||
}
|
||||
|
||||
return outputFiles;
|
||||
@@ -221,7 +219,7 @@ public class PipelineProcessor {
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
String apiKey = getApiKeyForUser();
|
||||
headers.add("X-API-Key", apiKey);
|
||||
headers.add("X-API-KEY", apiKey);
|
||||
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
|
||||
|
||||
// Create HttpEntity with the body and headers
|
||||
@@ -310,7 +308,7 @@ public class PipelineProcessor {
|
||||
|
||||
List<Resource> generateInputFiles(File[] files) throws Exception {
|
||||
if (files == null || files.length == 0) {
|
||||
logger.info("No files");
|
||||
log.info("No files");
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -318,7 +316,7 @@ public class PipelineProcessor {
|
||||
|
||||
for (File file : files) {
|
||||
Path path = Paths.get(file.getAbsolutePath());
|
||||
logger.info("Reading file: " + path); // debug statement
|
||||
log.info("Reading file: " + path); // debug statement
|
||||
|
||||
if (Files.exists(path)) {
|
||||
Resource fileResource =
|
||||
@@ -330,16 +328,16 @@ public class PipelineProcessor {
|
||||
};
|
||||
outputFiles.add(fileResource);
|
||||
} else {
|
||||
logger.info("File not found: " + path);
|
||||
log.info("File not found: " + path);
|
||||
}
|
||||
}
|
||||
logger.info("Files successfully loaded. Starting processing...");
|
||||
log.info("Files successfully loaded. Starting processing...");
|
||||
return outputFiles;
|
||||
}
|
||||
|
||||
List<Resource> generateInputFiles(MultipartFile[] files) throws Exception {
|
||||
if (files == null || files.length == 0) {
|
||||
logger.info("No files");
|
||||
log.info("No files");
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -355,7 +353,7 @@ public class PipelineProcessor {
|
||||
};
|
||||
outputFiles.add(fileResource);
|
||||
}
|
||||
logger.info("Files successfully loaded. Starting processing...");
|
||||
log.info("Files successfully loaded. Starting processing...");
|
||||
return outputFiles;
|
||||
}
|
||||
|
||||
@@ -369,7 +367,7 @@ public class PipelineProcessor {
|
||||
}
|
||||
|
||||
private List<Resource> unzip(byte[] data) throws IOException {
|
||||
logger.info("Unzipping data of length: {}", data.length);
|
||||
log.info("Unzipping data of length: {}", data.length);
|
||||
List<Resource> unzippedFiles = new ArrayList<>();
|
||||
|
||||
try (ByteArrayInputStream bais = new ByteArrayInputStream(data);
|
||||
@@ -396,7 +394,7 @@ public class PipelineProcessor {
|
||||
|
||||
// If the unzipped file is a zip file, unzip it
|
||||
if (isZip(baos.toByteArray())) {
|
||||
logger.info("File {} is a zip file. Unzipping...", filename);
|
||||
log.info("File {} is a zip file. Unzipping...", filename);
|
||||
unzippedFiles.addAll(unzip(baos.toByteArray()));
|
||||
} else {
|
||||
unzippedFiles.add(fileResource);
|
||||
@@ -404,7 +402,7 @@ public class PipelineProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("Unzipping completed. {} files were unzipped.", unzippedFiles.size());
|
||||
log.info("Unzipping completed. {} files were unzipped.", unzippedFiles.size());
|
||||
return unzippedFiles;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,8 +63,6 @@ import org.bouncycastle.operator.InputDecryptorProvider;
|
||||
import org.bouncycastle.operator.OperatorCreationException;
|
||||
import org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo;
|
||||
import org.bouncycastle.pkcs.PKCSException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
@@ -78,17 +76,17 @@ import io.github.pixee.security.Filenames;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.SPDF.model.api.security.SignPDFWithCertRequest;
|
||||
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/security")
|
||||
@Slf4j
|
||||
@Tag(name = "Security", description = "Security APIs")
|
||||
public class CertSignController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(CertSignController.class);
|
||||
|
||||
static {
|
||||
Security.addProvider(new BouncyCastleProvider());
|
||||
}
|
||||
@@ -108,7 +106,7 @@ public class CertSignController {
|
||||
logoFile = Files.createTempFile("signature", ".png").toFile();
|
||||
FileUtils.copyInputStreamToFile(is, logoFile);
|
||||
} catch (IOException e) {
|
||||
logger.error("Failed to load image signature file");
|
||||
log.error("Failed to load image signature file");
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
@@ -212,7 +210,9 @@ public class CertSignController {
|
||||
@Operation(
|
||||
summary = "Sign PDF with a Digital Certificate",
|
||||
description =
|
||||
"This endpoint accepts a PDF file, a digital certificate and related information to sign the PDF. It then returns the digitally signed PDF file. Input:PDF Output:PDF Type:SISO")
|
||||
"This endpoint accepts a PDF file, a digital certificate and related information to sign"
|
||||
+ " the PDF. It then returns the digitally signed PDF file. Input:PDF Output:PDF"
|
||||
+ " Type:SISO")
|
||||
public ResponseEntity<byte[]> signPDFWithCert(@ModelAttribute SignPDFWithCertRequest request)
|
||||
throws Exception {
|
||||
MultipartFile pdf = request.getFileInput();
|
||||
@@ -308,7 +308,7 @@ public class CertSignController {
|
||||
}
|
||||
doc.saveIncremental(output);
|
||||
} catch (Exception e) {
|
||||
logger.error("exception", e);
|
||||
log.error("exception", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -56,8 +56,6 @@ import org.apache.xmpbox.XMPMetadata;
|
||||
import org.apache.xmpbox.xml.DomXmpParser;
|
||||
import org.apache.xmpbox.xml.XmpParsingException;
|
||||
import org.apache.xmpbox.xml.XmpSerializer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
@@ -73,16 +71,16 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.SPDF.model.api.PDFFile;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/security")
|
||||
@Slf4j
|
||||
@Tag(name = "Security", description = "Security APIs")
|
||||
public class GetInfoOnPDF {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(GetInfoOnPDF.class);
|
||||
|
||||
static ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/get-info-on-pdf")
|
||||
@@ -224,7 +222,7 @@ public class GetInfoOnPDF {
|
||||
javascriptArray.add(jsNode);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.error("exception", e);
|
||||
log.error("exception", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -257,7 +255,7 @@ public class GetInfoOnPDF {
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// TODO Auto-generated catch block
|
||||
logger.error("exception", e);
|
||||
log.error("exception", e);
|
||||
}
|
||||
|
||||
boolean isPdfACompliant = checkForStandard(pdfBoxDoc, "PDF/A");
|
||||
@@ -309,7 +307,7 @@ public class GetInfoOnPDF {
|
||||
new XmpSerializer().serialize(xmpMeta, os, true);
|
||||
xmpString = new String(os.toByteArray(), StandardCharsets.UTF_8);
|
||||
} catch (XmpParsingException | IOException e) {
|
||||
logger.error("exception", e);
|
||||
log.error("exception", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -322,27 +320,14 @@ public class GetInfoOnPDF {
|
||||
PDEncryption pdfEncryption = pdfBoxDoc.getEncryption();
|
||||
encryption.put("EncryptionAlgorithm", pdfEncryption.getFilter());
|
||||
encryption.put("KeyLength", pdfEncryption.getLength());
|
||||
AccessPermission ap = pdfBoxDoc.getCurrentAccessPermission();
|
||||
if (ap != null) {
|
||||
ObjectNode permissionsNode = objectMapper.createObjectNode();
|
||||
|
||||
permissionsNode.put("CanAssembleDocument", ap.canAssembleDocument());
|
||||
permissionsNode.put("CanExtractContent", ap.canExtractContent());
|
||||
permissionsNode.put(
|
||||
"CanExtractForAccessibility", ap.canExtractForAccessibility());
|
||||
permissionsNode.put("CanFillInForm", ap.canFillInForm());
|
||||
permissionsNode.put("CanModify", ap.canModify());
|
||||
permissionsNode.put("CanModifyAnnotations", ap.canModifyAnnotations());
|
||||
permissionsNode.put("CanPrint", ap.canPrint());
|
||||
|
||||
encryption.set(
|
||||
"Permissions", permissionsNode); // set the node under "Permissions"
|
||||
}
|
||||
// Add other encryption-related properties as needed
|
||||
} else {
|
||||
encryption.put("IsEncrypted", false);
|
||||
}
|
||||
|
||||
ObjectNode permissionsNode = objectMapper.createObjectNode();
|
||||
setNodePermissions(pdfBoxDoc, permissionsNode);
|
||||
|
||||
ObjectNode pageInfoParent = objectMapper.createObjectNode();
|
||||
for (int pageNum = 0; pageNum < pdfBoxDoc.getNumberOfPages(); pageNum++) {
|
||||
ObjectNode pageInfo = objectMapper.createObjectNode();
|
||||
@@ -584,6 +569,7 @@ public class GetInfoOnPDF {
|
||||
jsonOutput.set("DocumentInfo", docInfoNode);
|
||||
jsonOutput.set("Compliancy", compliancy);
|
||||
jsonOutput.set("Encryption", encryption);
|
||||
jsonOutput.set("Permissions", permissionsNode); // set the node under "Permissions"
|
||||
jsonOutput.set("Other", other);
|
||||
jsonOutput.set("PerPageInfo", pageInfoParent);
|
||||
|
||||
@@ -597,11 +583,29 @@ public class GetInfoOnPDF {
|
||||
MediaType.APPLICATION_JSON);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("exception", e);
|
||||
log.error("exception", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void setNodePermissions(PDDocument pdfBoxDoc, ObjectNode permissionsNode) {
|
||||
AccessPermission ap = pdfBoxDoc.getCurrentAccessPermission();
|
||||
|
||||
permissionsNode.put("Document Assembly", getPermissionState(ap.canAssembleDocument()));
|
||||
permissionsNode.put("Extracting Content", getPermissionState(ap.canExtractContent()));
|
||||
permissionsNode.put(
|
||||
"Extracting for accessibility",
|
||||
getPermissionState(ap.canExtractForAccessibility()));
|
||||
permissionsNode.put("Form Filling", getPermissionState(ap.canFillInForm()));
|
||||
permissionsNode.put("Modifying", getPermissionState(ap.canModify()));
|
||||
permissionsNode.put("Modifying annotations", getPermissionState(ap.canModifyAnnotations()));
|
||||
permissionsNode.put("Printing", getPermissionState(ap.canPrint()));
|
||||
}
|
||||
|
||||
private String getPermissionState(boolean state) {
|
||||
return state ? "Allowed" : "Not Allowed";
|
||||
}
|
||||
|
||||
private static void addOutlinesToArray(PDOutlineItem outline, ArrayNode arrayNode) {
|
||||
if (outline == null) return;
|
||||
|
||||
@@ -695,7 +699,7 @@ public class GetInfoOnPDF {
|
||||
Exception
|
||||
e) { // Catching general exception for brevity, ideally you'd catch specific
|
||||
// exceptions.
|
||||
logger.error("exception", e);
|
||||
log.error("exception", e);
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
@@ -5,8 +5,6 @@ import java.io.IOException;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.encryption.AccessPermission;
|
||||
import org.apache.pdfbox.pdmodel.encryption.StandardProtectionPolicy;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
@@ -29,8 +27,6 @@ import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
@Tag(name = "Security", description = "Security APIs")
|
||||
public class PasswordController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(PasswordController.class);
|
||||
|
||||
private final CustomPDDocumentFactory pdfDocumentFactory;
|
||||
|
||||
@Autowired
|
||||
@@ -42,7 +38,8 @@ public class PasswordController {
|
||||
@Operation(
|
||||
summary = "Remove password from a PDF file",
|
||||
description =
|
||||
"This endpoint removes the password from a protected PDF file. Users need to provide the existing password. Input:PDF Output:PDF Type:SISO")
|
||||
"This endpoint removes the password from a protected PDF file. Users need to provide the"
|
||||
+ " existing password. Input:PDF Output:PDF Type:SISO")
|
||||
public ResponseEntity<byte[]> removePassword(@ModelAttribute PDFPasswordRequest request)
|
||||
throws IOException {
|
||||
MultipartFile fileInput = request.getFileInput();
|
||||
@@ -60,7 +57,8 @@ public class PasswordController {
|
||||
@Operation(
|
||||
summary = "Add password to a PDF file",
|
||||
description =
|
||||
"This endpoint adds password protection to a PDF file. Users can specify a set of permissions that should be applied to the file. Input:PDF Output:PDF")
|
||||
"This endpoint adds password protection to a PDF file. Users can specify a set of"
|
||||
+ " permissions that should be applied to the file. Input:PDF Output:PDF")
|
||||
public ResponseEntity<byte[]> addPassword(@ModelAttribute AddPasswordRequest request)
|
||||
throws IOException {
|
||||
MultipartFile fileInput = request.getFileInput();
|
||||
|
||||
@@ -8,8 +8,6 @@ import java.util.List;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
@@ -22,6 +20,7 @@ import io.github.pixee.security.Filenames;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.SPDF.model.PDFText;
|
||||
import stirling.software.SPDF.model.api.security.RedactPdfRequest;
|
||||
import stirling.software.SPDF.pdf.TextFinder;
|
||||
@@ -31,11 +30,10 @@ import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/security")
|
||||
@Slf4j
|
||||
@Tag(name = "Security", description = "Security APIs")
|
||||
public class RedactController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(RedactController.class);
|
||||
|
||||
private final CustomPDDocumentFactory pdfDocumentFactory;
|
||||
|
||||
@Autowired
|
||||
@@ -47,7 +45,8 @@ public class RedactController {
|
||||
@Operation(
|
||||
summary = "Redacts listOfText in a PDF document",
|
||||
description =
|
||||
"This operation takes an input PDF file and redacts the provided listOfText. Input:PDF, Output:PDF, Type:SISO")
|
||||
"This operation takes an input PDF file and redacts the provided listOfText. Input:PDF,"
|
||||
+ " Output:PDF, Type:SISO")
|
||||
public ResponseEntity<byte[]> redactPdf(@ModelAttribute RedactPdfRequest request)
|
||||
throws Exception {
|
||||
MultipartFile file = request.getFileInput();
|
||||
@@ -68,7 +67,7 @@ public class RedactController {
|
||||
}
|
||||
redactColor = Color.decode(colorString);
|
||||
} catch (NumberFormatException e) {
|
||||
logger.warn("Invalid color string provided. Using default color BLACK for redaction.");
|
||||
log.warn("Invalid color string provided. Using default color BLACK for redaction.");
|
||||
redactColor = Color.BLACK;
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user